diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b737661 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run tests + run: go test ./... -coverprofile=coverage.out -covermode=atomic + + - name: Validate coverage threshold + shell: bash + run: | + THRESHOLD=90 + EXIT_CODE=0 + + while read -r line; do + pkg=$(echo "$line" | awk '{print $2}') + pct=$(echo "$line" | sed -n 's/.*coverage: \([0-9.]*\)%.*/\1/p') + + if [ -z "$pct" ]; then + continue + fi + + pass=$(echo "$pct >= $THRESHOLD" | bc -l) + + if [ "$pass" -eq 1 ]; then + echo "OK: $pkg ${pct}%" + else + echo "FAIL: $pkg ${pct}% (minimum: ${THRESHOLD}%)" + EXIT_CODE=1 + fi + done < <(go test ./... -cover 2>&1) + + exit $EXIT_CODE diff --git a/.gitignore b/.gitignore index 220c0cf..42ce954 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .DS_Store - +.idea bin/ !bin/.gitkeep diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b5ef69e --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: format + +format: + docker-compose -f go-fmt.compose.yaml run --rm go-fmt format . \ No newline at end of file diff --git a/README.md b/README.md index 02aac54..935abe2 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,30 @@ # Collection -A Go port of Laravel's [Illuminate Collections](https://laravel.com/docs/collections) -- fluent, type-safe generic collections powered by Go generics. +[![Go Reference](https://pkg.go.dev/badge/github.com/gocanto/collection.svg)](https://pkg.go.dev/github.com/gocanto/collection) +[![Go Version](https://img.shields.io/badge/go-1.25-blue.svg)](https://golang.org/doc/devel/release.html#go1.25) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A powerful Go port of [Laravel Collections](https://laravel.com/docs/collections) β€” fluent, type-safe, and powered by Go generics and `iter.Seq` lazy evaluation. ```bash go get github.com/gocanto/collection ``` -Requires **Go 1.25** or later. +--- + +## ⚑️ Why use this? -## Overview +Go's slices and maps are powerful but often lead to repetitive boilerplate loops for common operations. `collection` provides a **fluent, expressive API** to handle data transformations with ease: -This library is a port of Laravel's [Illuminate\Support\Collection](https://github.com/laravel/framework/tree/master/src/Illuminate/Collections) to Go. It preserves the same rich API surface -- `filter`, `map`, `reduce`, `flatMap`, `chunk`, `partition`, and many more -- while redesigning everything idiomatically for Go with generics, `iter.Seq` for lazy evaluation, and full type safety. +- **πŸ’Ž Fluent Pipelines:** Chain `.Filter().Take().Each()` instead of nesting `for` loops. +- **πŸ›‘οΈ Type-Safe:** Built with Go genericsβ€”no `interface{}` casting or runtime type errors. +- **♻️ Immutable by Default:** Transformation methods return new collections, preserving your original data. +- **🐒 Lazy Evaluation:** Process massive datasets efficiently using `iter.Seq` sequences. +- **🧩 Modular:** Use the full fluent API or lightweight standalone utilities (`arr` and `kv`). -- **Readable data pipelines** -- chain `.Filter().Map().Take()` instead of nesting loops. -- **Type-safe generics** -- no `interface{}` casting; the compiler catches type errors at build time. -- **Immutable returns** -- methods return new collections, leaving the original untouched. -- **Lazy evaluation** -- process large or infinite datasets without allocating the entire result up front. -- **Standalone utilities** -- use individual functions from `arr` or `kv` without constructing a collection object. +--- -## Quick Start +## πŸš€ Quick Start ```go package main @@ -29,36 +35,51 @@ import ( ) func main() { + // 1. Create a collection from a slice numbers := collection.Collect([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) - result := numbers. - Filter(func(n int, _ int) bool { return n%2 == 0 }). + // 2. Chain operations fluently + even := numbers. + Filter(func(n int, _ int) bool { return n % 2 == 0 }). Take(3) - fmt.Println(result.All()) // [2, 4, 6] + // 3. Transform types using top-level generics + // (Go doesn't allow methods to introduce new type parameters) + result := collection.Map(even, func(n int, _ int) string { + return fmt.Sprintf("Number: %d", n) + }) + + fmt.Println(result.All()) // ["Number: 2", "Number: 4", "Number: 6"] } ``` -## Packages +--- + +## πŸ“¦ Package Ecosystem + +| Package | Purpose | Use when... | +|:---|:---|:---| +| **`collection`** | **The Core** | You want the full fluent `Collection[T]` experience for slices. | +| **`lazy`** | **Lazy Sequences** | You're handling large/infinite streams and want deferred execution. | +| **`collectible`** | **Key-Value Maps** | You need an ordered `collectible.Collection[K, V]` with a fluent API. | +| **`arr`** | **Slice Utils** | You need a quick one-off helper (e.g., `Sort`, `Flatten`) on a raw `[]T`. | +| **`kv`** | **Map Utils** | You need dot-notation access (`"user.profile.name"`) for `map[string]any`. | + +--- -| Package | Description | -|---------|-------------| -| `collection` | Core collection types: `Collection[T]`, `MapCollection[K,V]`, `LazyCollection[T]`, and helper functions. | -| `collection/arr` | Standalone generic slice utilities: `Flatten`, `Sort`, `Where`, `Map`, `Pluck`, `Partition`, etc. | -| `collection/kv` | Map utilities with dot-notation support: `Get`, `Set`, `Has`, `Dot`, `Undot`, `Only`, `Query`, etc. | +## πŸ“– Deep Dive -## Documentation +Explore our comprehensive documentation for each component: -Full API documentation is available in the [docs](docs/) directory: +- [**Architecture Overview**](docs/overview.md) β€” Design philosophy and core concepts. +- [**Collection API**](docs/collection.md) β€” Reference for the standard fluent collection. +- [**Collectible API**](docs/collectible.md) β€” Reference for key-value collection operations. +- [**Lazy API**](docs/lazy.md) β€” Reference for lazy sequences and `iter.Seq`. +- [**Array Utilities (`arr`)**](docs/arr.md) β€” Standalone functions for raw slices. +- [**Key-Value Utilities (`kv`)**](docs/kv.md) β€” Standalone functions for maps and dot-notation. -- [Overview](docs/overview.md) -- Architecture, design decisions, and usage guide. -- [Collection](docs/collection.md) -- `Collection[T]` API reference. -- [MapCollection](docs/map_collection.md) -- `MapCollection[K,V]` API reference. -- [LazyCollection](docs/lazy_collection.md) -- `LazyCollection[T]` API reference. -- [arr](docs/arr.md) -- Slice utilities API reference. -- [kv](docs/kv.md) -- Map utilities API reference. -- [Helpers](docs/helpers.md) -- Root-package helper functions. +--- -## License +## βš–οΈ License -MIT -- see [LICENSE](LICENSE) for details. +Released under the [MIT License](LICENSE). diff --git a/arr/access.go b/arr/access.go new file mode 100644 index 0000000..1375cb2 --- /dev/null +++ b/arr/access.go @@ -0,0 +1,146 @@ +package arr + +import "fmt" + +// First returns the first element that matches the optional callback. +// If no callback is provided, the first element of the slice is returned. +// The second return value reports whether a match was found. +func First[T any](items []T, callbacks ...func(T, int) bool) (T, bool) { + if len(callbacks) == 0 || callbacks[0] == nil { + if len(items) > 0 { + return items[0], true + } + + var zero T + + return zero, false + } + + callback := callbacks[0] + + for i, item := range items { + if callback(item, i) { + return item, true + } + } + + var zero T + + return zero, false +} + +// Last returns the last element that matches the optional callback. +// If no callback is provided, the last element of the slice is returned. +// The second return value reports whether a match was found. +func Last[T any](items []T, callbacks ...func(T, int) bool) (T, bool) { + if len(callbacks) == 0 || callbacks[0] == nil { + if len(items) > 0 { + return items[len(items)-1], true + } + + var zero T + + return zero, false + } + + callback := callbacks[0] + + for i := len(items) - 1; i >= 0; i-- { + if callback(items[i], i) { + return items[i], true + } + } + + var zero T + + return zero, false +} + +// Get returns the element at the given index. +// If the index is out of bounds, the first default value is returned, +// or the zero value of T. +func Get[T any](items []T, index int, defaults ...T) T { + if index >= 0 && index < len(items) { + return items[index] + } + + if len(defaults) > 0 { + return defaults[0] + } + + var zero T + + return zero +} + +// Sole returns the only element matching the optional callback. +// It returns an error if zero or more than one element matches. +func Sole[T any](items []T, callbacks ...func(T, int) bool) (T, error) { + if len(callbacks) == 0 || callbacks[0] == nil { + if len(items) == 1 { + return items[0], nil + } + + if len(items) == 0 { + var zero T + + return zero, fmt.Errorf("no items found") + } + + var zero T + + return zero, fmt.Errorf("multiple items found: %d items", len(items)) + } + + callback := callbacks[0] + + var result T + found := 0 + + for i, item := range items { + if callback(item, i) { + result = item + found++ + + if found > 1 { + var zero T + + return zero, fmt.Errorf("multiple items found: %d+ items", found) + } + } + } + + if found == 0 { + var zero T + + return zero, fmt.Errorf("no items found") + } + + return result, nil +} + +// Take returns up to limit elements from the slice. +// A positive limit takes from the front; a negative limit takes from the end. +func Take[T any](items []T, limit int) []T { + if limit < 0 { + start := len(items) + limit + + if start < 0 { + start = 0 + } + + result := make([]T, len(items)-start) + copy(result, items[start:]) + + return result + } + + if limit > len(items) { + limit = len(items) + } + + result := make([]T, limit) + copy(result, items[:limit]) + + return result +} diff --git a/arr/access_test.go b/arr/access_test.go new file mode 100644 index 0000000..b14807c --- /dev/null +++ b/arr/access_test.go @@ -0,0 +1,142 @@ +package arr_test + +import ( + "reflect" + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestFirst(t *testing.T) { + items := []int{1, 2, 3, 4, 5} + + v, ok := arr.First(items) + + if !ok || v != 1 { + t.Errorf("expected 1, got %d", v) + } + + v, ok = arr.First(items, func(item int, _ int) bool { return item > 3 }) + + if !ok || v != 4 { + t.Errorf("expected 4, got %d", v) + } + + _, ok = arr.First([]int{}) + + if ok { + t.Error("expected false for empty slice") + } +} + +func TestLast(t *testing.T) { + items := []int{1, 2, 3, 4, 5} + + v, ok := arr.Last(items) + + if !ok || v != 5 { + t.Errorf("expected 5, got %d", v) + } + + v, ok = arr.Last(items, func(item int, _ int) bool { return item < 4 }) + + if !ok || v != 3 { + t.Errorf("expected 3, got %d", v) + } + + _, ok = arr.Last([]int{}) + + if ok { + t.Error("expected false for empty slice") + } + + _, ok = arr.Last(items, func(item int, _ int) bool { return item > 100 }) + + if ok { + t.Error("expected false when callback finds no match") + } +} + +func TestGet(t *testing.T) { + items := []int{10, 20, 30} + + v := arr.Get(items, 1) + + if v != 20 { + t.Errorf("expected 20, got %d", v) + } + + v = arr.Get(items, 10, 99) + + if v != 99 { + t.Errorf("expected default 99, got %d", v) + } + + v = arr.Get(items, 10) + + if v != 0 { + t.Errorf("expected zero value 0, got %d", v) + } + + v = arr.Get(items, -1) + + if v != 0 { + t.Errorf("expected zero value for negative index, got %d", v) + } +} + +func TestSole(t *testing.T) { + v, err := arr.Sole([]int{42}) + + if err != nil || v != 42 { + t.Errorf("expected 42, got %d, err: %v", v, err) + } + + _, err = arr.Sole([]int{}) + + if err == nil { + t.Error("expected error for empty slice") + } + + _, err = arr.Sole([]int{1, 2, 3}) + + if err == nil { + t.Error("expected error for multiple items") + } + + v, err = arr.Sole([]int{1, 2, 3, 4, 5}, func(item int, _ int) bool { return item == 3 }) + + if err != nil || v != 3 { + t.Errorf("expected 3, got %d, err: %v", v, err) + } + + _, err = arr.Sole([]int{1, 2, 3}, func(item int, _ int) bool { return item > 10 }) + + if err == nil { + t.Error("expected error when callback finds no match") + } + + _, err = arr.Sole([]int{1, 2, 3}, func(item int, _ int) bool { return item > 1 }) + + if err == nil { + t.Error("expected error when callback finds multiple matches") + } +} + +func TestTake(t *testing.T) { + items := []int{1, 2, 3, 4, 5} + + result := arr.Take(items, 3) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } + + result = arr.Take(items, -2) + expected = []int{4, 5} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} diff --git a/arr/aggregate.go b/arr/aggregate.go new file mode 100644 index 0000000..98df2d2 --- /dev/null +++ b/arr/aggregate.go @@ -0,0 +1,23 @@ +package arr + +// Every report whether every element passes the callback test. +func Every[T any](items []T, callback func(T, int) bool) bool { + for i, item := range items { + if !callback(item, i) { + return false + } + } + + return true +} + +// Some reports whether any element passes the callback test. +func Some[T any](items []T, callback func(T, int) bool) bool { + for i, item := range items { + if callback(item, i) { + return true + } + } + + return false +} diff --git a/arr/aggregate_test.go b/arr/aggregate_test.go new file mode 100644 index 0000000..2592d63 --- /dev/null +++ b/arr/aggregate_test.go @@ -0,0 +1,31 @@ +package arr_test + +import ( + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestEvery(t *testing.T) { + items := []int{2, 4, 6} + + if !arr.Every(items, func(item int, _ int) bool { return item%2 == 0 }) { + t.Error("expected all even") + } + + if arr.Every([]int{2, 3, 4}, func(item int, _ int) bool { return item%2 == 0 }) { + t.Error("expected not all even") + } +} + +func TestSome(t *testing.T) { + items := []int{1, 2, 3} + + if !arr.Some(items, func(item int, _ int) bool { return item == 2 }) { + t.Error("expected to find 2") + } + + if arr.Some(items, func(item int, _ int) bool { return item == 10 }) { + t.Error("expected not to find 10") + } +} diff --git a/arr/check.go b/arr/check.go new file mode 100644 index 0000000..b30c2bc --- /dev/null +++ b/arr/check.go @@ -0,0 +1,39 @@ +package arr + +// Accessible reports whether the given value is non-nil. +func Accessible(value any) bool { + return value != nil +} + +// IsList reports whether the given slice is a list. +// In Go, all slices are sequential lists, so this always returns true. +func IsList[T any](items []T) bool { + return true +} + +// Exists -> it reports whether the given index is valid for the slice. +func Exists[T any](items []T, index int) bool { + return index >= 0 && index < len(items) +} + +// Has reports whether all the given indices are valid for the slice. +func Has[T any](items []T, indices ...int) bool { + for _, idx := range indices { + if idx < 0 || idx >= len(items) { + return false + } + } + + return true +} + +// HasAny reports whether at least one of the given indices is valid for the slice. +func HasAny[T any](items []T, indices ...int) bool { + for _, idx := range indices { + if idx >= 0 && idx < len(items) { + return true + } + } + + return false +} diff --git a/arr/check_test.go b/arr/check_test.go new file mode 100644 index 0000000..cff2536 --- /dev/null +++ b/arr/check_test.go @@ -0,0 +1,59 @@ +package arr_test + +import ( + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestAccessible(t *testing.T) { + if !arr.Accessible([]int{1, 2, 3}) { + t.Error("expected true for non-nil slice") + } + + if arr.Accessible(nil) { + t.Error("expected false for nil") + } +} + +func TestIsList(t *testing.T) { + if !arr.IsList([]int{1, 2, 3}) { + t.Error("expected true") + } +} + +func TestExists(t *testing.T) { + items := []int{10, 20, 30} + + if !arr.Exists(items, 1) { + t.Error("expected index 1 to exist") + } + + if arr.Exists(items, 10) { + t.Error("expected index 10 to not exist") + } +} + +func TestHas(t *testing.T) { + items := []int{10, 20, 30} + + if !arr.Has(items, 0, 1, 2) { + t.Error("expected all indices to exist") + } + + if arr.Has(items, 0, 5) { + t.Error("expected to fail for index 5") + } +} + +func TestHasAny(t *testing.T) { + items := []int{10, 20, 30} + + if !arr.HasAny(items, 0, 99) { + t.Error("expected at least one valid index") + } + + if arr.HasAny(items, 5, 10) { + t.Error("expected no valid indices") + } +} diff --git a/arr/combine.go b/arr/combine.go new file mode 100644 index 0000000..b969b87 --- /dev/null +++ b/arr/combine.go @@ -0,0 +1,40 @@ +package arr + +// Flatten flattens a slice of slices into a single slice. +func Flatten[T any](items [][]T) []T { + result := make([]T, 0) + + for _, inner := range items { + result = append(result, inner...) + } + + return result +} + +// Collapse merges a slice of slices into a single slice. +// It is an alias for [Flatten]. +func Collapse[T any](items [][]T) []T { + return Flatten(items) +} + +// CrossJoin returns the Cartesian product of the given slices. +func CrossJoin[T any](lists ...[]T) [][]T { + results := [][]T{{}} + + for _, list := range lists { + var newResults [][]T + + for _, result := range results { + for _, item := range list { + newResult := make([]T, len(result)+1) + copy(newResult, result) + newResult[len(result)] = item + newResults = append(newResults, newResult) + } + } + + results = newResults + } + + return results +} diff --git a/arr/combine_test.go b/arr/combine_test.go new file mode 100644 index 0000000..2590441 --- /dev/null +++ b/arr/combine_test.go @@ -0,0 +1,35 @@ +package arr_test + +import ( + "reflect" + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestFlatten(t *testing.T) { + items := [][]int{{1, 2}, {3, 4}, {5}} + result := arr.Flatten(items) + expected := []int{1, 2, 3, 4, 5} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestCollapse(t *testing.T) { + items := [][]int{{1, 2}, {3, 4}} + result := arr.Collapse(items) + + if len(result) != 4 { + t.Errorf("expected 4 items, got %d", len(result)) + } +} + +func TestCrossJoin(t *testing.T) { + result := arr.CrossJoin([]int{1, 2}, []int{10, 20}) + + if len(result) != 4 { + t.Errorf("expected 4 combinations, got %d", len(result)) + } +} diff --git a/arr/filter.go b/arr/filter.go new file mode 100644 index 0000000..04484d0 --- /dev/null +++ b/arr/filter.go @@ -0,0 +1,84 @@ +package arr + +// Only returns the elements at the given indices. +func Only[T any](items []T, indices []int) []T { + result := make([]T, 0, len(indices)) + + for _, idx := range indices { + if idx >= 0 && idx < len(items) { + result = append(result, items[idx]) + } + } + + return result +} + +// Except returns all elements except those at the given indices. +func Except[T any](items []T, indices []int) []T { + excludeSet := make(map[int]bool, len(indices)) + + for _, idx := range indices { + excludeSet[idx] = true + } + + result := make([]T, 0) + + for i, item := range items { + if !excludeSet[i] { + result = append(result, item) + } + } + + return result +} + +// Where returns the elements for which the callback returns true. +func Where[T any](items []T, callback func(T, int) bool) []T { + result := make([]T, 0) + + for i, item := range items { + if callback(item, i) { + result = append(result, item) + } + } + + return result +} + +// WhereNotNull returns all elements that are not the zero value of their type. +func WhereNotNull[T comparable](items []T) []T { + var zero T + result := make([]T, 0) + + for _, item := range items { + if item != zero { + result = append(result, item) + } + } + + return result +} + +// Reject returns the elements for which the callback returns false. +func Reject[T any](items []T, callback func(T, int) bool) []T { + return Where(items, func(item T, index int) bool { + return !callback(item, index) + }) +} + +// Partition splits elements into two slices: those that pass the callback +// and those that do not. +func Partition[T any](items []T, callback func(T, int) bool) ([]T, []T) { + pass := make([]T, 0) + fail := make([]T, 0) + + for i, item := range items { + if callback(item, i) { + pass = append(pass, item) + } else { + fail = append(fail, item) + } + } + + return pass, fail +} diff --git a/arr/filter_test.go b/arr/filter_test.go new file mode 100644 index 0000000..ebbf332 --- /dev/null +++ b/arr/filter_test.go @@ -0,0 +1,71 @@ +package arr_test + +import ( + "reflect" + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestOnly(t *testing.T) { + items := []int{10, 20, 30, 40, 50} + result := arr.Only(items, []int{1, 3}) + expected := []int{20, 40} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestExcept(t *testing.T) { + items := []int{10, 20, 30, 40, 50} + result := arr.Except(items, []int{1, 3}) + expected := []int{10, 30, 50} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestWhere(t *testing.T) { + items := []int{1, 2, 3, 4, 5} + result := arr.Where(items, func(item int, _ int) bool { return item%2 == 0 }) + expected := []int{2, 4} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestWhereNotNull(t *testing.T) { + items := []string{"a", "", "b", "", "c"} + result := arr.WhereNotNull(items) + expected := []string{"a", "b", "c"} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestReject(t *testing.T) { + items := []int{1, 2, 3, 4, 5} + result := arr.Reject(items, func(item int, _ int) bool { return item%2 == 0 }) + expected := []int{1, 3, 5} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestPartition(t *testing.T) { + items := []int{1, 2, 3, 4, 5} + pass, fail := arr.Partition(items, func(item int, _ int) bool { return item > 3 }) + + if !reflect.DeepEqual(pass, []int{4, 5}) { + t.Errorf("expected [4 5], got %v", pass) + } + + if !reflect.DeepEqual(fail, []int{1, 2, 3}) { + t.Errorf("expected [1 2 3], got %v", fail) + } +} diff --git a/arr/helpers.go b/arr/helpers.go new file mode 100644 index 0000000..fa2377f --- /dev/null +++ b/arr/helpers.go @@ -0,0 +1,35 @@ +package arr + +import "strings" + +// Wrap wraps the given value in a single-element slice. +func Wrap[T any](value T) []T { + return []T{value} +} + +// WrapSlice returns the given slice unchanged. +// Use this to wrap a value that is already a slice without nesting it. +func WrapSlice[T any](value []T) []T { + return value +} + +// Join concatenates string slice elements with a glue string. +// Optional final glue is used between the last two elements. +func Join(items []string, glue string, finalGlues ...string) string { + if len(items) == 0 { + return "" + } + + if len(items) == 1 { + return items[0] + } + + if len(finalGlues) > 0 && finalGlues[0] != "" { + last := items[len(items)-1] + rest := items[:len(items)-1] + + return strings.Join(rest, glue) + finalGlues[0] + last + } + + return strings.Join(items, glue) +} diff --git a/arr/helpers_test.go b/arr/helpers_test.go new file mode 100644 index 0000000..b4181ed --- /dev/null +++ b/arr/helpers_test.go @@ -0,0 +1,51 @@ +package arr_test + +import ( + "reflect" + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestWrap(t *testing.T) { + result := arr.Wrap(42) + + if !reflect.DeepEqual(result, []int{42}) { + t.Errorf("expected [42], got %v", result) + } +} + +func TestWrapSlice(t *testing.T) { + input := []int{1, 2, 3} + result := arr.WrapSlice(input) + + if !reflect.DeepEqual(result, input) { + t.Errorf("expected %v, got %v", input, result) + } +} + +func TestJoin(t *testing.T) { + result := arr.Join([]string{"a", "b", "c"}, ", ") + + if result != "a, b, c" { + t.Errorf("expected 'a, b, c', got '%s'", result) + } + + result = arr.Join([]string{"a", "b", "c"}, ", ", " and ") + + if result != "a, b and c" { + t.Errorf("expected 'a, b and c', got '%s'", result) + } + + result = arr.Join([]string{}, ", ") + + if result != "" { + t.Errorf("expected empty string, got '%s'", result) + } + + result = arr.Join([]string{"solo"}, ", ") + + if result != "solo" { + t.Errorf("expected 'solo', got '%s'", result) + } +} diff --git a/arr/mutation.go b/arr/mutation.go new file mode 100644 index 0000000..5088ca5 --- /dev/null +++ b/arr/mutation.go @@ -0,0 +1,65 @@ +package arr + +// Prepend inserts an element at the beginning of the slice and returns the new slice. +func Prepend[T any](items []T, value T) []T { + return append([]T{value}, items...) +} + +// Push appends one or more elements to the end of the slice and returns the new slice. +func Push[T any](items []T, values ...T) []T { + return append(items, values...) +} + +// Set returns a new slice with the element at the given index replaced. +// If the index is out of bounds, the original slice is returned unchanged. +func Set[T any](items []T, index int, value T) []T { + if index < 0 || index >= len(items) { + result := make([]T, len(items)) + copy(result, items) + + return result + } + + result := make([]T, len(items)) + copy(result, items) + result[index] = value + + return result +} + +// Forget returns a new slice with the element at the given index removed. +// If the index is out of bounds, the original slice is returned unchanged. +func Forget[T any](items []T, index int) []T { + if index < 0 || index >= len(items) { + result := make([]T, len(items)) + copy(result, items) + + return result + } + + result := make([]T, 0, len(items)-1) + result = append(result, items[:index]...) + result = append(result, items[index+1:]...) + + return result +} + +// Pull removes the element at the given index from the slice and returns both +// the removed element and the remaining slice. If the index is out of bounds, +// the zero value and the original slice are returned. +func Pull[T any](items []T, index int) (T, []T) { + if index < 0 || index >= len(items) { + var zero T + result := make([]T, len(items)) + copy(result, items) + + return zero, result + } + + value := items[index] + result := make([]T, 0, len(items)-1) + result = append(result, items[:index]...) + result = append(result, items[index+1:]...) + + return value, result +} diff --git a/arr/mutation_test.go b/arr/mutation_test.go new file mode 100644 index 0000000..8908df5 --- /dev/null +++ b/arr/mutation_test.go @@ -0,0 +1,110 @@ +package arr_test + +import ( + "reflect" + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestPrepend(t *testing.T) { + items := []int{2, 3} + result := arr.Prepend(items, 1) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestPush(t *testing.T) { + items := []int{1, 2} + result := arr.Push(items, 3, 4) + expected := []int{1, 2, 3, 4} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestSet(t *testing.T) { + items := []int{10, 20, 30} + + result := arr.Set(items, 1, 99) + expected := []int{10, 99, 30} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } + + result = arr.Set(items, 10, 99) + + if !reflect.DeepEqual(result, items) { + t.Errorf("expected unchanged %v, got %v", items, result) + } + + result = arr.Set(items, -1, 99) + + if !reflect.DeepEqual(result, items) { + t.Errorf("expected unchanged %v for negative index, got %v", items, result) + } +} + +func TestForget(t *testing.T) { + items := []int{10, 20, 30} + + result := arr.Forget(items, 1) + expected := []int{10, 30} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } + + result = arr.Forget(items, 10) + + if !reflect.DeepEqual(result, items) { + t.Errorf("expected unchanged %v, got %v", items, result) + } + + result = arr.Forget(items, -1) + + if !reflect.DeepEqual(result, items) { + t.Errorf("expected unchanged %v for negative index, got %v", items, result) + } +} + +func TestPull(t *testing.T) { + items := []int{10, 20, 30} + + v, result := arr.Pull(items, 1) + + if v != 20 { + t.Errorf("expected pulled value 20, got %d", v) + } + + expected := []int{10, 30} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } + + v, result = arr.Pull(items, 10) + + if v != 0 { + t.Errorf("expected zero value, got %d", v) + } + + if !reflect.DeepEqual(result, items) { + t.Errorf("expected unchanged %v, got %v", items, result) + } + + v, result = arr.Pull(items, -1) + + if v != 0 { + t.Errorf("expected zero value for negative index, got %d", v) + } + + if !reflect.DeepEqual(result, items) { + t.Errorf("expected unchanged %v, got %v", items, result) + } +} diff --git a/arr/slice.go b/arr/slice.go deleted file mode 100644 index 5dec6f1..0000000 --- a/arr/slice.go +++ /dev/null @@ -1,470 +0,0 @@ -package arr - -import ( - "fmt" - "math/rand/v2" - "sort" - "strings" -) - -// Accessible reports whether the given value is non-nil. -func Accessible(value any) bool { - return value != nil -} - -// IsList reports whether the given slice is a list. -// In Go, all slices are sequential lists, so this always returns true. -func IsList[T any](items []T) bool { - return true -} - -// First returns the first element that matches the optional callback. -// If no callback is provided, the first element of the slice is returned. -// The second return value reports whether a match was found. -func First[T any](items []T, callbacks ...func(T, int) bool) (T, bool) { - if len(callbacks) == 0 || callbacks[0] == nil { - if len(items) > 0 { - return items[0], true - } - var zero T - return zero, false - } - callback := callbacks[0] - for i, item := range items { - if callback(item, i) { - return item, true - } - } - var zero T - return zero, false -} - -// Last returns the last element that matches the optional callback. -// If no callback is provided, the last element of the slice is returned. -// The second return value reports whether a match was found. -func Last[T any](items []T, callbacks ...func(T, int) bool) (T, bool) { - if len(callbacks) == 0 || callbacks[0] == nil { - if len(items) > 0 { - return items[len(items)-1], true - } - var zero T - return zero, false - } - callback := callbacks[0] - for i := len(items) - 1; i >= 0; i-- { - if callback(items[i], i) { - return items[i], true - } - } - var zero T - return zero, false -} - -// Take returns up to limit elements from the slice. -// A positive limit takes from the front; a negative limit takes from the end. -func Take[T any](items []T, limit int) []T { - if limit < 0 { - start := len(items) + limit - if start < 0 { - start = 0 - } - result := make([]T, len(items)-start) - copy(result, items[start:]) - return result - } - if limit > len(items) { - limit = len(items) - } - result := make([]T, limit) - copy(result, items[:limit]) - return result -} - -// Only returns the elements at the given indices. -func Only[T any](items []T, indices []int) []T { - result := make([]T, 0, len(indices)) - for _, idx := range indices { - if idx >= 0 && idx < len(items) { - result = append(result, items[idx]) - } - } - return result -} - -// Except returns all elements except those at the given indices. -func Except[T any](items []T, indices []int) []T { - excludeSet := make(map[int]bool, len(indices)) - for _, idx := range indices { - excludeSet[idx] = true - } - result := make([]T, 0) - for i, item := range items { - if !excludeSet[i] { - result = append(result, item) - } - } - return result -} - -// Flatten flattens a slice of slices into a single slice. -func Flatten[T any](items [][]T) []T { - result := make([]T, 0) - for _, inner := range items { - result = append(result, inner...) - } - return result -} - -// Collapse merges a slice of slices into a single slice. -// It is an alias for [Flatten]. -func Collapse[T any](items [][]T) []T { - return Flatten(items) -} - -// Wrap wraps the given value in a single-element slice. -func Wrap[T any](value T) []T { - return []T{value} -} - -// WrapSlice returns the given slice unchanged. -// Use this to wrap a value that is already a slice without nesting it. -func WrapSlice[T any](value []T) []T { - return value -} - -// Prepend inserts an element at the beginning of the slice and returns the new slice. -func Prepend[T any](items []T, value T) []T { - return append([]T{value}, items...) -} - -// Push appends one or more elements to the end of the slice and returns the new slice. -func Push[T any](items []T, values ...T) []T { - return append(items, values...) -} - -// Shuffle returns a new slice with the elements in random order. -func Shuffle[T any](items []T) []T { - result := make([]T, len(items)) - copy(result, items) - rand.Shuffle(len(result), func(i, j int) { - result[i], result[j] = result[j], result[i] - }) - return result -} - -// Random returns a new slice containing count random elements. -// If count is omitted it defaults to 1. -func Random[T any](items []T, counts ...int) []T { - count := 1 - if len(counts) > 0 { - count = counts[0] - } - shuffled := Shuffle(items) - if count >= len(shuffled) { - return shuffled - } - return shuffled[:count] -} - -// Sort returns a new slice sorted using the provided less function. -// The sort is stable. -func Sort[T any](items []T, less func(a, b T) bool) []T { - result := make([]T, len(items)) - copy(result, items) - sort.SliceStable(result, func(i, j int) bool { - return less(result[i], result[j]) - }) - return result -} - -// SortDesc returns a new slice sorted in descending order using the provided less function. -func SortDesc[T any](items []T, less func(a, b T) bool) []T { - return Sort(items, func(a, b T) bool { - return less(b, a) - }) -} - -// SortRecursive sorts a slice using the provided less function. -// For flat slices this behaves identically to [Sort]. -func SortRecursive[T any](items []T, less func(a, b T) bool) []T { - return Sort(items, less) -} - -// Where returns the elements for which the callback returns true. -func Where[T any](items []T, callback func(T, int) bool) []T { - result := make([]T, 0) - for i, item := range items { - if callback(item, i) { - result = append(result, item) - } - } - return result -} - -// WhereNotNull returns all elements that are not the zero value of their type. -func WhereNotNull[T comparable](items []T) []T { - var zero T - result := make([]T, 0) - for _, item := range items { - if item != zero { - result = append(result, item) - } - } - return result -} - -// Reject returns the elements for which the callback returns false. -func Reject[T any](items []T, callback func(T, int) bool) []T { - return Where(items, func(item T, index int) bool { - return !callback(item, index) - }) -} - -// Partition splits elements into two slices: those that pass the callback -// and those that do not. -func Partition[T any](items []T, callback func(T, int) bool) ([]T, []T) { - pass := make([]T, 0) - fail := make([]T, 0) - for i, item := range items { - if callback(item, i) { - pass = append(pass, item) - } else { - fail = append(fail, item) - } - } - return pass, fail -} - -// Every reports whether every element passes the callback test. -func Every[T any](items []T, callback func(T, int) bool) bool { - for i, item := range items { - if !callback(item, i) { - return false - } - } - return true -} - -// Some reports whether any element passes the callback test. -func Some[T any](items []T, callback func(T, int) bool) bool { - for i, item := range items { - if callback(item, i) { - return true - } - } - return false -} - -// Exists reports whether the given index is valid for the slice. -func Exists[T any](items []T, index int) bool { - return index >= 0 && index < len(items) -} - -// Has reports whether all of the given indices are valid for the slice. -func Has[T any](items []T, indices ...int) bool { - for _, idx := range indices { - if idx < 0 || idx >= len(items) { - return false - } - } - return true -} - -// HasAny reports whether at least one of the given indices is valid for the slice. -func HasAny[T any](items []T, indices ...int) bool { - for _, idx := range indices { - if idx >= 0 && idx < len(items) { - return true - } - } - return false -} - -// Join concatenates string slice elements with a glue string. -// An optional final glue is used between the last two elements. -func Join(items []string, glue string, finalGlues ...string) string { - if len(items) == 0 { - return "" - } - if len(items) == 1 { - return items[0] - } - if len(finalGlues) > 0 && finalGlues[0] != "" { - last := items[len(items)-1] - rest := items[:len(items)-1] - return strings.Join(rest, glue) + finalGlues[0] + last - } - return strings.Join(items, glue) -} - -// CrossJoin returns the Cartesian product of the given slices. -func CrossJoin[T any](lists ...[]T) [][]T { - results := [][]T{{}} - for _, list := range lists { - var newResults [][]T - for _, result := range results { - for _, item := range list { - newResult := make([]T, len(result)+1) - copy(newResult, result) - newResult[len(result)] = item - newResults = append(newResults, newResult) - } - } - results = newResults - } - return results -} - -// Divide returns two slices: one containing the indices and one containing the values. -func Divide[T any](items []T) ([]int, []T) { - keys := make([]int, len(items)) - values := make([]T, len(items)) - for i, item := range items { - keys[i] = i - values[i] = item - } - return keys, values -} - -// Map applies a callback to each element and returns a slice of the results. -func Map[T any, R any](items []T, callback func(T, int) R) []R { - result := make([]R, len(items)) - for i, item := range items { - result[i] = callback(item, i) - } - return result -} - -// MapWithKeys applies a callback to each element, producing key-value pairs -// that are collected into a map. -func MapWithKeys[T any, K comparable, V any](items []T, callback func(T) (K, V)) map[K]V { - result := make(map[K]V) - for _, item := range items { - key, value := callback(item) - result[key] = value - } - return result -} - -// MapSpread applies a callback to each element and returns a slice of the results. -// In Go this is identical to [Map]. -func MapSpread[T any, R any](items []T, callback func(T, int) R) []R { - return Map(items, callback) -} - -// KeyBy indexes the slice elements by the key returned from keyFunc. -func KeyBy[T any, K comparable](items []T, keyFunc func(T) K) map[K]T { - result := make(map[K]T) - for _, item := range items { - result[keyFunc(item)] = item - } - return result -} - -// Pluck extracts a value from each element using valueFunc and returns -// the collected values as a slice. -func Pluck[T any, V any](items []T, valueFunc func(T) V) []V { - result := make([]V, len(items)) - for i, item := range items { - result[i] = valueFunc(item) - } - return result -} - -// Get returns the element at the given index. -// If the index is out of bounds, the first default value is returned, -// or the zero value of T. -func Get[T any](items []T, index int, defaults ...T) T { - if index >= 0 && index < len(items) { - return items[index] - } - if len(defaults) > 0 { - return defaults[0] - } - var zero T - return zero -} - -// Set returns a new slice with the element at the given index replaced. -// If the index is out of bounds, the original slice is returned unchanged. -func Set[T any](items []T, index int, value T) []T { - if index < 0 || index >= len(items) { - result := make([]T, len(items)) - copy(result, items) - return result - } - result := make([]T, len(items)) - copy(result, items) - result[index] = value - return result -} - -// Forget returns a new slice with the element at the given index removed. -// If the index is out of bounds, the original slice is returned unchanged. -func Forget[T any](items []T, index int) []T { - if index < 0 || index >= len(items) { - result := make([]T, len(items)) - copy(result, items) - return result - } - result := make([]T, 0, len(items)-1) - result = append(result, items[:index]...) - result = append(result, items[index+1:]...) - return result -} - -// Pull removes the element at the given index from the slice and returns both -// the removed element and the remaining slice. If the index is out of bounds, -// the zero value and the original slice are returned. -func Pull[T any](items []T, index int) (T, []T) { - if index < 0 || index >= len(items) { - var zero T - result := make([]T, len(items)) - copy(result, items) - return zero, result - } - value := items[index] - result := make([]T, 0, len(items)-1) - result = append(result, items[:index]...) - result = append(result, items[index+1:]...) - return value, result -} - -// Sole returns the only element matching the optional callback. -// It returns an error if zero or more than one element matches. -func Sole[T any](items []T, callbacks ...func(T, int) bool) (T, error) { - if len(callbacks) == 0 || callbacks[0] == nil { - if len(items) == 1 { - return items[0], nil - } - if len(items) == 0 { - var zero T - return zero, fmt.Errorf("no items found") - } - var zero T - return zero, fmt.Errorf("multiple items found: %d items", len(items)) - } - callback := callbacks[0] - var result T - found := 0 - for i, item := range items { - if callback(item, i) { - result = item - found++ - if found > 1 { - var zero T - return zero, fmt.Errorf("multiple items found: %d+ items", found) - } - } - } - if found == 0 { - var zero T - return zero, fmt.Errorf("no items found") - } - return result, nil -} - -// SortRecursiveDesc sorts a slice in descending order using the provided less function. -func SortRecursiveDesc[T any](items []T, less func(a, b T) bool) []T { - return SortDesc(items, less) -} diff --git a/arr/slice_test.go b/arr/slice_test.go deleted file mode 100644 index 74a0e61..0000000 --- a/arr/slice_test.go +++ /dev/null @@ -1,355 +0,0 @@ -package arr_test - -import ( - "reflect" - "testing" - - "github.com/gocanto/collection/arr" -) - -func TestAccessible(t *testing.T) { - if !arr.Accessible([]int{1, 2, 3}) { - t.Error("expected true for non-nil slice") - } - if arr.Accessible(nil) { - t.Error("expected false for nil") - } -} - -func TestIsList(t *testing.T) { - if !arr.IsList([]int{1, 2, 3}) { - t.Error("expected true") - } -} - -func TestFirst(t *testing.T) { - items := []int{1, 2, 3, 4, 5} - - v, ok := arr.First(items) - if !ok || v != 1 { - t.Errorf("expected 1, got %d", v) - } - - v, ok = arr.First(items, func(item int, _ int) bool { return item > 3 }) - if !ok || v != 4 { - t.Errorf("expected 4, got %d", v) - } - - _, ok = arr.First([]int{}) - if ok { - t.Error("expected false for empty slice") - } -} - -func TestLast(t *testing.T) { - items := []int{1, 2, 3, 4, 5} - - v, ok := arr.Last(items) - if !ok || v != 5 { - t.Errorf("expected 5, got %d", v) - } - - v, ok = arr.Last(items, func(item int, _ int) bool { return item < 4 }) - if !ok || v != 3 { - t.Errorf("expected 3, got %d", v) - } -} - -func TestTake(t *testing.T) { - items := []int{1, 2, 3, 4, 5} - - result := arr.Take(items, 3) - expected := []int{1, 2, 3} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } - - result = arr.Take(items, -2) - expected = []int{4, 5} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestOnly(t *testing.T) { - items := []int{10, 20, 30, 40, 50} - result := arr.Only(items, []int{1, 3}) - expected := []int{20, 40} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestExcept(t *testing.T) { - items := []int{10, 20, 30, 40, 50} - result := arr.Except(items, []int{1, 3}) - expected := []int{10, 30, 50} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestFlatten(t *testing.T) { - items := [][]int{{1, 2}, {3, 4}, {5}} - result := arr.Flatten(items) - expected := []int{1, 2, 3, 4, 5} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestCollapse(t *testing.T) { - items := [][]int{{1, 2}, {3, 4}} - result := arr.Collapse(items) - if len(result) != 4 { - t.Errorf("expected 4 items, got %d", len(result)) - } -} - -func TestPrepend(t *testing.T) { - items := []int{2, 3} - result := arr.Prepend(items, 1) - expected := []int{1, 2, 3} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestPush(t *testing.T) { - items := []int{1, 2} - result := arr.Push(items, 3, 4) - expected := []int{1, 2, 3, 4} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestShuffle(t *testing.T) { - items := []int{1, 2, 3, 4, 5} - result := arr.Shuffle(items) - if len(result) != 5 { - t.Errorf("expected 5 items, got %d", len(result)) - } -} - -func TestRandom(t *testing.T) { - items := []int{1, 2, 3, 4, 5} - result := arr.Random(items, 2) - if len(result) != 2 { - t.Errorf("expected 2 items, got %d", len(result)) - } -} - -func TestSort(t *testing.T) { - items := []int{3, 1, 4, 1, 5} - result := arr.Sort(items, func(a, b int) bool { return a < b }) - expected := []int{1, 1, 3, 4, 5} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestSortDesc(t *testing.T) { - items := []int{3, 1, 4, 1, 5} - result := arr.SortDesc(items, func(a, b int) bool { return a < b }) - expected := []int{5, 4, 3, 1, 1} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestWhere(t *testing.T) { - items := []int{1, 2, 3, 4, 5} - result := arr.Where(items, func(item int, _ int) bool { return item%2 == 0 }) - expected := []int{2, 4} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestWhereNotNull(t *testing.T) { - items := []string{"a", "", "b", "", "c"} - result := arr.WhereNotNull(items) - expected := []string{"a", "b", "c"} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestReject(t *testing.T) { - items := []int{1, 2, 3, 4, 5} - result := arr.Reject(items, func(item int, _ int) bool { return item%2 == 0 }) - expected := []int{1, 3, 5} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestPartition(t *testing.T) { - items := []int{1, 2, 3, 4, 5} - pass, fail := arr.Partition(items, func(item int, _ int) bool { return item > 3 }) - if !reflect.DeepEqual(pass, []int{4, 5}) { - t.Errorf("expected [4 5], got %v", pass) - } - if !reflect.DeepEqual(fail, []int{1, 2, 3}) { - t.Errorf("expected [1 2 3], got %v", fail) - } -} - -func TestEvery(t *testing.T) { - items := []int{2, 4, 6} - if !arr.Every(items, func(item int, _ int) bool { return item%2 == 0 }) { - t.Error("expected all even") - } - if arr.Every([]int{2, 3, 4}, func(item int, _ int) bool { return item%2 == 0 }) { - t.Error("expected not all even") - } -} - -func TestSome(t *testing.T) { - items := []int{1, 2, 3} - if !arr.Some(items, func(item int, _ int) bool { return item == 2 }) { - t.Error("expected to find 2") - } - if arr.Some(items, func(item int, _ int) bool { return item == 10 }) { - t.Error("expected not to find 10") - } -} - -func TestExists(t *testing.T) { - items := []int{10, 20, 30} - if !arr.Exists(items, 1) { - t.Error("expected index 1 to exist") - } - if arr.Exists(items, 10) { - t.Error("expected index 10 to not exist") - } -} - -func TestHas(t *testing.T) { - items := []int{10, 20, 30} - if !arr.Has(items, 0, 1, 2) { - t.Error("expected all indices to exist") - } - if arr.Has(items, 0, 5) { - t.Error("expected to fail for index 5") - } -} - -func TestHasAny(t *testing.T) { - items := []int{10, 20, 30} - if !arr.HasAny(items, 0, 99) { - t.Error("expected at least one valid index") - } - if arr.HasAny(items, 5, 10) { - t.Error("expected no valid indices") - } -} - -func TestJoin(t *testing.T) { - result := arr.Join([]string{"a", "b", "c"}, ", ") - if result != "a, b, c" { - t.Errorf("expected 'a, b, c', got '%s'", result) - } - - result = arr.Join([]string{"a", "b", "c"}, ", ", " and ") - if result != "a, b and c" { - t.Errorf("expected 'a, b and c', got '%s'", result) - } -} - -func TestCrossJoin(t *testing.T) { - result := arr.CrossJoin([]int{1, 2}, []int{10, 20}) - if len(result) != 4 { - t.Errorf("expected 4 combinations, got %d", len(result)) - } -} - -func TestDivide(t *testing.T) { - items := []string{"a", "b", "c"} - keys, values := arr.Divide(items) - if !reflect.DeepEqual(keys, []int{0, 1, 2}) { - t.Errorf("expected [0 1 2], got %v", keys) - } - if !reflect.DeepEqual(values, items) { - t.Errorf("expected %v, got %v", items, values) - } -} - -func TestMap(t *testing.T) { - items := []int{1, 2, 3} - result := arr.Map(items, func(item int, _ int) int { return item * 2 }) - expected := []int{2, 4, 6} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestMapWithKeys(t *testing.T) { - type User struct { - ID int - Name string - } - users := []User{{1, "Alice"}, {2, "Bob"}} - result := arr.MapWithKeys(users, func(u User) (int, string) { return u.ID, u.Name }) - if result[1] != "Alice" { - t.Error("expected Alice at key 1") - } -} - -func TestKeyBy(t *testing.T) { - type User struct { - ID int - Name string - } - users := []User{{1, "Alice"}, {2, "Bob"}} - result := arr.KeyBy(users, func(u User) int { return u.ID }) - if result[1].Name != "Alice" { - t.Error("expected Alice") - } -} - -func TestPluck(t *testing.T) { - type User struct { - Name string - } - users := []User{{"Alice"}, {"Bob"}} - result := arr.Pluck(users, func(u User) string { return u.Name }) - expected := []string{"Alice", "Bob"} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestWrap(t *testing.T) { - result := arr.Wrap(42) - if !reflect.DeepEqual(result, []int{42}) { - t.Errorf("expected [42], got %v", result) - } -} - -func TestWrapSlice(t *testing.T) { - input := []int{1, 2, 3} - result := arr.WrapSlice(input) - if !reflect.DeepEqual(result, input) { - t.Errorf("expected %v, got %v", input, result) - } -} - -func TestMapSpread(t *testing.T) { - items := []int{1, 2, 3} - result := arr.MapSpread(items, func(item int, _ int) int { return item * 10 }) - expected := []int{10, 20, 30} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} - -func TestSortRecursive(t *testing.T) { - items := []int{3, 1, 2} - result := arr.SortRecursive(items, func(a, b int) bool { return a < b }) - expected := []int{1, 2, 3} - if !reflect.DeepEqual(result, expected) { - t.Errorf("expected %v, got %v", expected, result) - } -} diff --git a/arr/sorting.go b/arr/sorting.go new file mode 100644 index 0000000..778044f --- /dev/null +++ b/arr/sorting.go @@ -0,0 +1,67 @@ +package arr + +import ( + "math/rand/v2" + "sort" +) + +// Sort returns a new slice sorted using the provided less function. +// The sort is stable. +func Sort[T any](items []T, less func(a, b T) bool) []T { + result := make([]T, len(items)) + copy(result, items) + + sort.SliceStable(result, func(i, j int) bool { + return less(result[i], result[j]) + }) + + return result +} + +// SortDesc returns a new slice sorted in descending order using the provided less function. +func SortDesc[T any](items []T, less func(a, b T) bool) []T { + return Sort(items, func(a, b T) bool { + return less(b, a) + }) +} + +// SortRecursive sorts a slice using the provided less function. +// For flat slices this behaves identically to [Sort]. +func SortRecursive[T any](items []T, less func(a, b T) bool) []T { + return Sort(items, less) +} + +// SortRecursiveDesc sorts a slice in descending order using the provided less function. +func SortRecursiveDesc[T any](items []T, less func(a, b T) bool) []T { + return SortDesc(items, less) +} + +// Shuffle returns a new slice with the elements in random order. +func Shuffle[T any](items []T) []T { + result := make([]T, len(items)) + copy(result, items) + + rand.Shuffle(len(result), func(i, j int) { + result[i], result[j] = result[j], result[i] + }) + + return result +} + +// Random returns a new slice containing count random elements. +// If the count is omitted, it defaults to 1. +func Random[T any](items []T, counts ...int) []T { + count := 1 + + if len(counts) > 0 { + count = counts[0] + } + + shuffled := Shuffle(items) + + if count >= len(shuffled) { + return shuffled + } + + return shuffled[:count] +} diff --git a/arr/sorting_test.go b/arr/sorting_test.go new file mode 100644 index 0000000..e7151c5 --- /dev/null +++ b/arr/sorting_test.go @@ -0,0 +1,66 @@ +package arr_test + +import ( + "reflect" + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestSort(t *testing.T) { + items := []int{3, 1, 4, 1, 5} + result := arr.Sort(items, func(a, b int) bool { return a < b }) + expected := []int{1, 1, 3, 4, 5} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestSortDesc(t *testing.T) { + items := []int{3, 1, 4, 1, 5} + result := arr.SortDesc(items, func(a, b int) bool { return a < b }) + expected := []int{5, 4, 3, 1, 1} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestSortRecursive(t *testing.T) { + items := []int{3, 1, 2} + result := arr.SortRecursive(items, func(a, b int) bool { return a < b }) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestSortRecursiveDesc(t *testing.T) { + items := []int{1, 3, 2} + result := arr.SortRecursiveDesc(items, func(a, b int) bool { return a < b }) + expected := []int{3, 2, 1} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestShuffle(t *testing.T) { + items := []int{1, 2, 3, 4, 5} + result := arr.Shuffle(items) + + if len(result) != 5 { + t.Errorf("expected 5 items, got %d", len(result)) + } +} + +func TestRandom(t *testing.T) { + items := []int{1, 2, 3, 4, 5} + result := arr.Random(items, 2) + + if len(result) != 2 { + t.Errorf("expected 2 items, got %d", len(result)) + } +} diff --git a/arr/transform.go b/arr/transform.go new file mode 100644 index 0000000..804c500 --- /dev/null +++ b/arr/transform.go @@ -0,0 +1,67 @@ +package arr + +// Map applies a callback to each element and returns a slice of the results. +func Map[T any, R any](items []T, callback func(T, int) R) []R { + result := make([]R, len(items)) + + for i, item := range items { + result[i] = callback(item, i) + } + + return result +} + +// MapWithKeys applies a callback to each element, producing key-value pairs +// that are collected into a map. +func MapWithKeys[T any, K comparable, V any](items []T, callback func(T) (K, V)) map[K]V { + result := make(map[K]V) + + for _, item := range items { + key, value := callback(item) + result[key] = value + } + + return result +} + +// MapSpread applies a callback to each element and returns a slice of the results. +// In Go this is identical to [Map]. +func MapSpread[T any, R any](items []T, callback func(T, int) R) []R { + return Map(items, callback) +} + +// KeyBy indexes the slice elements by the key returned from keyFunc. +func KeyBy[T any, K comparable](items []T, keyFunc func(T) K) map[K]T { + result := make(map[K]T) + + for _, item := range items { + result[keyFunc(item)] = item + } + + return result +} + +// Pluck extracts a value from each element using valueFunc and returns +// the collected values as a slice. +func Pluck[T any, V any](items []T, valueFunc func(T) V) []V { + result := make([]V, len(items)) + + for i, item := range items { + result[i] = valueFunc(item) + } + + return result +} + +// Divide returns two slices: one containing the indices and one containing the values. +func Divide[T any](items []T) ([]int, []T) { + keys := make([]int, len(items)) + values := make([]T, len(items)) + + for i, item := range items { + keys[i] = i + values[i] = item + } + + return keys, values +} diff --git a/arr/transform_test.go b/arr/transform_test.go new file mode 100644 index 0000000..df832d7 --- /dev/null +++ b/arr/transform_test.go @@ -0,0 +1,83 @@ +package arr_test + +import ( + "reflect" + "testing" + + "github.com/gocanto/collection/arr" +) + +func TestMap(t *testing.T) { + items := []int{1, 2, 3} + result := arr.Map(items, func(item int, _ int) int { return item * 2 }) + expected := []int{2, 4, 6} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestMapWithKeys(t *testing.T) { + type User struct { + ID int + Name string + } + + users := []User{{1, "Alice"}, {2, "Bob"}} + result := arr.MapWithKeys(users, func(u User) (int, string) { return u.ID, u.Name }) + + if result[1] != "Alice" { + t.Error("expected Alice at key 1") + } +} + +func TestMapSpread(t *testing.T) { + items := []int{1, 2, 3} + result := arr.MapSpread(items, func(item int, _ int) int { return item * 10 }) + expected := []int{10, 20, 30} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestKeyBy(t *testing.T) { + type User struct { + ID int + Name string + } + + users := []User{{1, "Alice"}, {2, "Bob"}} + result := arr.KeyBy(users, func(u User) int { return u.ID }) + + if result[1].Name != "Alice" { + t.Error("expected Alice") + } +} + +func TestPluck(t *testing.T) { + type User struct { + Name string + } + + users := []User{{"Alice"}, {"Bob"}} + result := arr.Pluck(users, func(u User) string { return u.Name }) + expected := []string{"Alice", "Bob"} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestDivide(t *testing.T) { + items := []string{"a", "b", "c"} + keys, values := arr.Divide(items) + + if !reflect.DeepEqual(keys, []int{0, 1, 2}) { + t.Errorf("expected [0 1 2], got %v", keys) + } + + if !reflect.DeepEqual(values, items) { + t.Errorf("expected %v, got %v", items, values) + } +} diff --git a/collectible/access.go b/collectible/access.go new file mode 100644 index 0000000..ae6a215 --- /dev/null +++ b/collectible/access.go @@ -0,0 +1,141 @@ +package collectible + +// Get returns the value for the given key. The second return value indicates +// whether the key was found. An optional default may be provided. +func (m *Collection[K, V]) Get(key K, defaults ...V) (V, bool) { + if v, ok := m.items[key]; ok { + return v, true + } + + if len(defaults) > 0 { + return defaults[0], false + } + + var zero V + + return zero, false +} + +// Has reports whether the given key exists in the collection. +func (m *Collection[K, V]) Has(key K) bool { + _, ok := m.items[key] + + return ok +} + +// HasAny reports whether any of the given keys exist in the collection. +func (m *Collection[K, V]) HasAny(keys ...K) bool { + for _, k := range keys { + if m.Has(k) { + return true + } + } + + return false +} + +// Contains reports whether any item in the collection satisfies the predicate. +func (m *Collection[K, V]) Contains(predicate func(V, K) bool) bool { + for _, k := range m.keys { + if predicate(m.items[k], k) { + return true + } + } + + return false +} + +// Some is an alias for Contains. +func (m *Collection[K, V]) Some(predicate func(V, K) bool) bool { + return m.Contains(predicate) +} + +// DoesntContain reports whether no item in the collection satisfies the predicate. +func (m *Collection[K, V]) DoesntContain(predicate func(V, K) bool) bool { + return !m.Contains(predicate) +} + +// HasSole reports whether exactly one item in the collection satisfies the predicate. +func (m *Collection[K, V]) HasSole(predicate func(V, K) bool) bool { + count := 0 + + for _, k := range m.keys { + if predicate(m.items[k], k) { + count++ + + if count > 1 { + return false + } + } + } + + return count == 1 +} + +// Search returns the key of the first item that satisfies the predicate. +// The second return value indicates whether a match was found. +func (m *Collection[K, V]) Search(predicate func(V, K) bool) (K, bool) { + for _, k := range m.keys { + if predicate(m.items[k], k) { + return k, true + } + } + + var zero K + + return zero, false +} + +// First returns the first value that satisfies the optional predicate. If no +// predicate is given, the first item in the insertion order is returned. +func (m *Collection[K, V]) First(predicates ...func(V, K) bool) (V, bool) { + if len(m.keys) == 0 { + var zero V + + return zero, false + } + + if len(predicates) == 0 || predicates[0] == nil { + return m.items[m.keys[0]], true + } + + predicate := predicates[0] + + for _, k := range m.keys { + if predicate(m.items[k], k) { + return m.items[k], true + } + } + + var zero V + + return zero, false +} + +// Last returns the last value that satisfies the optional predicate. If no +// predicate is given, the last item in the insertion order is returned. +func (m *Collection[K, V]) Last(predicates ...func(V, K) bool) (V, bool) { + if len(m.keys) == 0 { + var zero V + + return zero, false + } + + if len(predicates) == 0 || predicates[0] == nil { + return m.items[m.keys[len(m.keys)-1]], true + } + + predicate := predicates[0] + + for i := len(m.keys) - 1; i >= 0; i-- { + k := m.keys[i] + + if predicate(m.items[k], k) { + return m.items[k], true + } + } + + var zero V + + return zero, false +} diff --git a/collectible/collection.go b/collectible/collection.go new file mode 100644 index 0000000..afb45c8 --- /dev/null +++ b/collectible/collection.go @@ -0,0 +1,112 @@ +package collectible + +import ( + "iter" + + "github.com/gocanto/collection/support" +) + +// Collection wraps a map and provides a fluent API for working with +// key-value pairs while maintaining insertion order. +type Collection[K comparable, V any] struct { + items map[K]V + keys []K // maintains insertion order +} + +// New creates a new Collection from a map. +func New[K comparable, V any](items map[K]V) *Collection[K, V] { + if items == nil { + items = make(map[K]V) + } + + keys := make([]K, 0, len(items)) + + for k := range items { + keys = append(keys, k) + } + + return &Collection[K, V]{items: items, keys: keys} +} + +// FromPairs creates a Collection from key-value pairs. +func FromPairs[K comparable, V any](pairs ...support.Pair[K, V]) *Collection[K, V] { + items := make(map[K]V, len(pairs)) + keys := make([]K, 0, len(pairs)) + + for _, p := range pairs { + if _, exists := items[p.Key]; !exists { + keys = append(keys, p.Key) + } + + items[p.Key] = p.Value + } + + return &Collection[K, V]{items: items, keys: keys} +} + +// All returns a shallow copy of the underlying map. +func (m *Collection[K, V]) All() map[K]V { + result := make(map[K]V, len(m.items)) + + for k, v := range m.items { + result[k] = v + } + + return result +} + +// Keys returns all keys as a slice, preserving insertion order. +func (m *Collection[K, V]) Keys() []K { + result := make([]K, len(m.keys)) + copy(result, m.keys) + + return result +} + +// Values returns all values as a slice, preserving insertion order. +func (m *Collection[K, V]) Values() []V { + result := make([]V, 0, len(m.keys)) + + for _, k := range m.keys { + result = append(result, m.items[k]) + } + + return result +} + +// Count returns the number of items in the collection. +func (m *Collection[K, V]) Count() int { + return len(m.items) +} + +// IsEmpty reports whether the collection contains no items. +func (m *Collection[K, V]) IsEmpty() bool { + return len(m.items) == 0 +} + +// IsNotEmpty reports whether the collection contains at least one item. +func (m *Collection[K, V]) IsNotEmpty() bool { + return len(m.items) > 0 +} + +// ContainsOneItem reports whether the collection contains exactly one item. +func (m *Collection[K, V]) ContainsOneItem() bool { + return len(m.items) == 1 +} + +// ContainsManyItems reports whether the collection contains more than one item. +func (m *Collection[K, V]) ContainsManyItems() bool { + return len(m.items) > 1 +} + +// Iter returns an iterator over the collection's key-value pairs in insertion +// order. +func (m *Collection[K, V]) Iter() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, k := range m.keys { + if !yield(k, m.items[k]) { + return + } + } + } +} diff --git a/collectible/collection_test.go b/collectible/collection_test.go new file mode 100644 index 0000000..639af8f --- /dev/null +++ b/collectible/collection_test.go @@ -0,0 +1,874 @@ +package collectible + +import ( + "reflect" + "strings" + "testing" + + "github.com/gocanto/collection/support" +) + +func TestNew(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + + if m.Count() != 3 { + t.Errorf("expected 3, got %d", m.Count()) + } +} + +func TestFromPairs(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + v, ok := m.Get("a") + + if !ok || v != 1 { + t.Errorf("expected 1, got %d", v) + } +} + +func TestMapGet(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + + v, ok := m.Get("a") + + if !ok || v != 1 { + t.Errorf("expected 1, got %d", v) + } + + _, ok = m.Get("z") + + if ok { + t.Error("expected not found") + } + + v, _ = m.Get("z", 99) + + if v != 99 { + t.Errorf("expected default 99, got %d", v) + } +} + +func TestMapGetOrPut(t *testing.T) { + m := New(map[string]int{"a": 1}) + + v := m.GetOrPut("a", 99) + + if v != 1 { + t.Errorf("expected existing 1, got %d", v) + } + + v = m.GetOrPut("b", 99) + + if v != 99 { + t.Errorf("expected default 99, got %d", v) + } + + if m.Count() != 2 { + t.Errorf("expected 2 items, got %d", m.Count()) + } +} + +func TestMapHas(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + + if !m.Has("a") { + t.Error("expected to have 'a'") + } + + if m.Has("z") { + t.Error("expected not to have 'z'") + } +} + +func TestMapHasAny(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + + if !m.HasAny("z", "a") { + t.Error("expected HasAny to return true") + } + + if m.HasAny("x", "y", "z") { + t.Error("expected HasAny to return false") + } +} + +func TestMapPut(t *testing.T) { + m := New(map[string]int{}) + m.Put("a", 1) + v, ok := m.Get("a") + + if !ok || v != 1 { + t.Errorf("expected 1, got %d", v) + } +} + +func TestMapPull(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + v, ok := m.Pull("a") + + if !ok || v != 1 { + t.Errorf("expected 1, got %d", v) + } + + if m.Has("a") { + t.Error("expected 'a' to be removed") + } +} + +func TestMapForget(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + m.Forget("a", "c") + + if m.Count() != 1 { + t.Errorf("expected 1, got %d", m.Count()) + } +} + +func TestMapOnly(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + result := m.Only("a", "c") + + if result.Count() != 2 { + t.Errorf("expected 2, got %d", result.Count()) + } + + if !result.Has("a") || !result.Has("c") { + t.Error("expected to have 'a' and 'c'") + } +} + +func TestMapExcept(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + result := m.Except("b") + + if result.Count() != 2 { + t.Errorf("expected 2, got %d", result.Count()) + } + + if result.Has("b") { + t.Error("expected 'b' to be excluded") + } +} + +func TestMapKeys(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + keys := m.Keys() + + if len(keys) != 2 { + t.Errorf("expected 2 keys, got %d", len(keys)) + } +} + +func TestMapValues(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + values := m.Values() + + if len(values) != 2 { + t.Errorf("expected 2 values, got %d", len(values)) + } +} + +func TestMapContains(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + + if !m.Contains(func(v int, k string) bool { return v == 2 }) { + t.Error("expected to contain value 2") + } + + if m.Contains(func(v int, k string) bool { return v == 99 }) { + t.Error("expected not to contain 99") + } +} + +func TestMapFirst(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + v, ok := m.First() + + if !ok || v != 1 { + t.Errorf("expected 1, got %d", v) + } +} + +func TestMapLast(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + v, ok := m.Last() + + if !ok || v != 2 { + t.Errorf("expected 2, got %d", v) + } +} + +func TestMapFilter(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + filtered := m.Filter(func(v int, k string) bool { return v > 1 }) + + if filtered.Count() != 2 { + t.Errorf("expected 2, got %d", filtered.Count()) + } +} + +func TestMapReject(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + rejected := m.Reject(func(v int, k string) bool { return v > 1 }) + + if rejected.Count() != 1 { + t.Errorf("expected 1, got %d", rejected.Count()) + } +} + +func TestMapValues_Transform(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + result := MapValues(m, func(v int, k string) int { return v * 10 }) + v, _ := result.Get("a") + + if v != 10 { + t.Errorf("expected 10, got %d", v) + } +} + +func TestMapEvery(t *testing.T) { + m := New(map[string]int{"a": 2, "b": 4}) + + if !m.Every(func(v int, k string) bool { return v%2 == 0 }) { + t.Error("expected all even") + } +} + +func TestMapPartition(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + pass, fail := m.Partition(func(v int, k string) bool { return v > 1 }) + + if pass.Count() != 2 { + t.Errorf("expected 2 passing, got %d", pass.Count()) + } + + if fail.Count() != 1 { + t.Errorf("expected 1 failing, got %d", fail.Count()) + } +} + +func TestMapMerge(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + result := m.Merge(map[string]int{"b": 20, "c": 3}) + v, _ := result.Get("b") + + if v != 20 { + t.Errorf("expected 20 (overwritten), got %d", v) + } + + if result.Count() != 3 { + t.Errorf("expected 3, got %d", result.Count()) + } +} + +func TestMapUnion(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + result := m.Union(map[string]int{"b": 20, "c": 3}) + v, _ := result.Get("b") + + if v != 2 { + t.Errorf("expected 2 (not overwritten), got %d", v) + } + + if result.Count() != 3 { + t.Errorf("expected 3, got %d", result.Count()) + } +} + +func TestMapDiffKeys(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + result := m.DiffKeys(map[string]int{"b": 99}) + + if result.Has("b") { + t.Error("expected 'b' to be excluded") + } + + if result.Count() != 2 { + t.Errorf("expected 2, got %d", result.Count()) + } +} + +func TestMapIntersectByKeys(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + result := m.IntersectByKeys(map[string]int{"a": 99, "c": 99}) + + if result.Count() != 2 { + t.Errorf("expected 2, got %d", result.Count()) + } +} + +func TestFlip(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + flipped := Flip(m) + v, ok := flipped.Get(1) + + if !ok || v != "a" { + t.Errorf("expected 'a', got '%s'", v) + } +} + +func TestSortKeys(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "c", Value: 3}, + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + sorted := SortKeys(m) + keys := sorted.Keys() + expected := []string{"a", "b", "c"} + + if !reflect.DeepEqual(keys, expected) { + t.Errorf("expected %v, got %v", expected, keys) + } +} + +func TestSortKeysDesc(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "c", Value: 3}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + sorted := SortKeysDesc(m) + keys := sorted.Keys() + expected := []string{"c", "b", "a"} + + if !reflect.DeepEqual(keys, expected) { + t.Errorf("expected %v, got %v", expected, keys) + } +} + +func TestMapImplode(t *testing.T) { + m := FromPairs( + support.Pair[string, string]{Key: "a", Value: "hello"}, + support.Pair[string, string]{Key: "b", Value: "world"}, + ) + result := m.Implode(", ") + + if result != "hello, world" { + t.Errorf("expected 'hello, world', got '%s'", result) + } +} + +func TestMapToJSON(t *testing.T) { + m := New(map[string]int{"a": 1}) + b, err := m.ToJSON() + + if err != nil { + t.Fatal(err) + } + + if string(b) != `{"a":1}` { + t.Errorf("expected {\"a\":1}, got %s", string(b)) + } +} + +func TestMapCopy(t *testing.T) { + m := New(map[string]int{"a": 1}) + m2 := m.Copy() + m.Put("b", 2) + + if m2.Has("b") { + t.Error("copy should not be affected") + } +} + +func TestMapToPairs(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + pairs := m.ToPairs() + + if len(pairs) != 2 { + t.Errorf("expected 2 pairs, got %d", len(pairs)) + } +} + +func TestMapWhen(t *testing.T) { + m := New(map[string]int{"a": 1}) + result := m.When(true, func(mc *Collection[string, int]) *Collection[string, int] { + mc.Put("b", 2) + + return mc + }) + + if !result.Has("b") { + t.Error("expected 'b' to be added") + } +} + +func TestMapSearch(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + key, ok := m.Search(func(v int, k string) bool { return v == 2 }) + + if !ok || key != "b" { + t.Errorf("expected 'b', got '%s'", key) + } +} + +func TestMapIsEmpty(t *testing.T) { + m := New(map[string]int{}) + + if !m.IsEmpty() { + t.Error("expected empty") + } + + if m.IsNotEmpty() { + t.Error("expected empty") + } +} + +func TestMapContainsOneItem(t *testing.T) { + m := New(map[string]int{"a": 1}) + + if !m.ContainsOneItem() { + t.Error("expected true") + } +} + +func TestMapContainsManyItems(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + + if !m.ContainsManyItems() { + t.Error("expected true") + } +} + +func TestMapEach(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + sum := 0 + m.Each(func(v int, k string) bool { + sum += v + + return true + }) + + if sum != 3 { + t.Errorf("expected 3, got %d", sum) + } +} + +func TestMapDoesntContain(t *testing.T) { + m := New(map[string]int{"a": 1}) + + if !m.DoesntContain(func(v int, k string) bool { return v == 99 }) { + t.Error("expected true") + } +} + +func TestMapSortKeysUsing(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "banana", Value: 1}, + support.Pair[string, int]{Key: "apple", Value: 2}, + support.Pair[string, int]{Key: "cherry", Value: 3}, + ) + sorted := m.SortKeysUsing(func(a, b string) bool { return a < b }) + keys := sorted.Keys() + expected := []string{"apple", "banana", "cherry"} + + if !reflect.DeepEqual(keys, expected) { + t.Errorf("expected %v, got %v", expected, keys) + } +} + +func TestMapReplace(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + result := m.Replace(map[string]int{"b": 20, "c": 3}) + v, _ := result.Get("b") + + if v != 20 { + t.Errorf("expected 20, got %d", v) + } +} + +func TestMapSome(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2}) + + if !m.Some(func(v int, k string) bool { return v == 2 }) { + t.Error("expected Some to return true") + } + + if m.Some(func(v int, k string) bool { return v == 99 }) { + t.Error("expected Some to return false") + } +} + +func TestMapHasSole(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + + if !m.HasSole(func(v int, k string) bool { return v == 2 }) { + t.Error("expected true for single match") + } + + if m.HasSole(func(v int, k string) bool { return v > 1 }) { + t.Error("expected false for multiple matches") + } + + if m.HasSole(func(v int, k string) bool { return v > 10 }) { + t.Error("expected false for no matches") + } +} + +func TestMapFirstWithPredicate(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + support.Pair[string, int]{Key: "c", Value: 3}, + ) + + v, ok := m.First(func(v int, k string) bool { return v > 1 }) + + if !ok || v != 2 { + t.Errorf("expected 2, got %d", v) + } + + _, ok = m.First(func(v int, k string) bool { return v > 10 }) + + if ok { + t.Error("expected false when no match") + } + + empty := New(map[string]int{}) + + _, ok = empty.First() + + if ok { + t.Error("expected false for empty collection") + } +} + +func TestMapLastWithPredicate(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + support.Pair[string, int]{Key: "c", Value: 3}, + ) + + v, ok := m.Last(func(v int, k string) bool { return v < 3 }) + + if !ok || v != 2 { + t.Errorf("expected 2, got %d", v) + } + + _, ok = m.Last(func(v int, k string) bool { return v > 10 }) + + if ok { + t.Error("expected false when no match") + } + + empty := New(map[string]int{}) + + _, ok = empty.Last() + + if ok { + t.Error("expected false for empty collection") + } +} + +func TestMapSearchNotFound(t *testing.T) { + m := New(map[string]int{"a": 1}) + + _, ok := m.Search(func(v int, k string) bool { return v == 99 }) + + if ok { + t.Error("expected false when no match") + } +} + +func TestMapEachEarlyReturn(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + support.Pair[string, int]{Key: "c", Value: 3}, + ) + count := 0 + m.Each(func(v int, k string) bool { + count++ + + return count < 2 + }) + + if count != 2 { + t.Errorf("expected 2 iterations, got %d", count) + } +} + +func TestMapEveryEmpty(t *testing.T) { + m := New(map[string]int{}) + + if !m.Every(func(v int, k string) bool { return v > 0 }) { + t.Error("expected true for empty collection") + } +} + +func TestMergeRecursive(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + ) + result := MergeRecursive(m, map[string]int{"b": 2}) + + if result.Count() != 2 { + t.Errorf("expected 2, got %d", result.Count()) + } +} + +func TestMapDiffKeysUsing(t *testing.T) { + m := New(map[string]int{"a": 1, "b": 2, "c": 3}) + result := m.DiffKeysUsing(map[string]int{"A": 99}, func(k1, k2 string) bool { + return strings.EqualFold(k1, k2) + }) + + if result.Has("a") { + t.Error("expected 'a' to be excluded via case-insensitive match") + } + + if result.Count() != 2 { + t.Errorf("expected 2, got %d", result.Count()) + } +} + +func TestMapDiffAssoc(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + support.Pair[string, int]{Key: "c", Value: 3}, + ) + result := DiffAssoc(m, map[string]int{"a": 1, "b": 99}) + + if result.Has("a") { + t.Error("expected 'a' excluded (same key and value)") + } + + if !result.Has("b") { + t.Error("expected 'b' included (value differs)") + } + + if !result.Has("c") { + t.Error("expected 'c' included (key missing from items)") + } +} + +func TestMapIntersectAssoc(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + support.Pair[string, int]{Key: "c", Value: 3}, + ) + result := IntersectAssoc(m, map[string]int{"a": 1, "b": 99}) + + if !result.Has("a") { + t.Error("expected 'a' included (key and value match)") + } + + if result.Has("b") { + t.Error("expected 'b' excluded (value differs)") + } + + if result.Has("c") { + t.Error("expected 'c' excluded (key missing)") + } + + if result.Count() != 1 { + t.Errorf("expected 1, got %d", result.Count()) + } +} + +func TestMapJoin(t *testing.T) { + m := FromPairs( + support.Pair[string, string]{Key: "a", Value: "hello"}, + support.Pair[string, string]{Key: "b", Value: "beautiful"}, + support.Pair[string, string]{Key: "c", Value: "world"}, + ) + result := m.Join(", ", " and ") + + if result != "hello, beautiful and world" { + t.Errorf("expected 'hello, beautiful and world', got '%s'", result) + } + + result = m.Join(", ") + + if result != "hello, beautiful, world" { + t.Errorf("expected 'hello, beautiful, world', got '%s'", result) + } +} + +func TestMapTap(t *testing.T) { + m := New(map[string]int{"a": 1}) + called := false + result := m.Tap(func(mc *Collection[string, int]) { + called = true + }) + + if !called { + t.Error("expected callback to be called") + } + + if result != m { + t.Error("expected Tap to return self") + } +} + +func TestMapUnless(t *testing.T) { + m := New(map[string]int{"a": 1}) + result := m.Unless(false, func(mc *Collection[string, int]) *Collection[string, int] { + mc.Put("b", 2) + + return mc + }) + + if !result.Has("b") { + t.Error("expected callback to be applied when condition is false") + } + + m2 := New(map[string]int{"a": 1}) + result2 := m2.Unless(true, func(mc *Collection[string, int]) *Collection[string, int] { + mc.Put("b", 2) + + return mc + }) + + if result2.Has("b") { + t.Error("expected callback not applied when condition is true") + } +} + +func TestMapWhenFalseWithDefault(t *testing.T) { + m := New(map[string]int{"a": 1}) + result := m.When(false, func(mc *Collection[string, int]) *Collection[string, int] { + mc.Put("b", 2) + + return mc + }, func(mc *Collection[string, int]) *Collection[string, int] { + mc.Put("c", 3) + + return mc + }) + + if result.Has("b") { + t.Error("expected primary callback not called") + } + + if !result.Has("c") { + t.Error("expected default callback applied") + } +} + +func TestMapWhenFalseWithoutDefault(t *testing.T) { + m := New(map[string]int{"a": 1}) + result := m.When(false, func(mc *Collection[string, int]) *Collection[string, int] { + mc.Put("b", 2) + + return mc + }) + + if result.Has("b") { + t.Error("expected callback not applied") + } + + if result.Count() != 1 { + t.Errorf("expected 1, got %d", result.Count()) + } +} + +func TestMapToPrettyJSON(t *testing.T) { + m := New(map[string]int{"a": 1}) + b, err := m.ToPrettyJSON() + + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(string(b), "\n") { + t.Error("expected pretty JSON with newlines") + } +} + +func TestMapString(t *testing.T) { + m := New(map[string]int{"a": 1}) + s := m.String() + + if s != `{"a":1}` { + t.Errorf("expected {\"a\":1}, got %s", s) + } +} + +func TestMapMarshalJSON(t *testing.T) { + m := New(map[string]int{"a": 1}) + b, err := m.MarshalJSON() + + if err != nil { + t.Fatal(err) + } + + if string(b) != `{"a":1}` { + t.Errorf("expected {\"a\":1}, got %s", string(b)) + } +} + +func TestMapUnmarshalJSON(t *testing.T) { + m := New(map[string]int{}) + err := m.UnmarshalJSON([]byte(`{"a":1,"b":2}`)) + + if err != nil { + t.Fatal(err) + } + + v, ok := m.Get("a") + + if !ok || v != 1 { + t.Errorf("expected 1, got %d", v) + } +} + +func TestMapDump(t *testing.T) { + m := New(map[string]int{"a": 1}) + result := m.Dump() + + if result != m { + t.Error("expected Dump to return self") + } +} + +func TestMapIter(t *testing.T) { + m := FromPairs( + support.Pair[string, int]{Key: "a", Value: 1}, + support.Pair[string, int]{Key: "b", Value: 2}, + ) + sum := 0 + + for _, v := range m.Iter() { + sum += v + } + + if sum != 3 { + t.Errorf("expected 3, got %d", sum) + } +} + +func TestMapNewNil(t *testing.T) { + m := New[string, int](nil) + + if !m.IsEmpty() { + t.Error("expected empty collection from nil map") + } +} diff --git a/collectible/combine.go b/collectible/combine.go new file mode 100644 index 0000000..2be1d35 --- /dev/null +++ b/collectible/combine.go @@ -0,0 +1,165 @@ +package collectible + +// Merge returns a new collection with the given map merged in. Existing keys +// are overwritten by the incoming values. +func (m *Collection[K, V]) Merge(items map[K]V) *Collection[K, V] { + result := make(map[K]V, len(m.items)+len(items)) + newKeys := make([]K, len(m.keys)) + copy(newKeys, m.keys) + + for k, v := range m.items { + result[k] = v + } + + for k, v := range items { + if _, exists := result[k]; !exists { + newKeys = append(newKeys, k) + } + + result[k] = v + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// Union returns a new collection that is the union of the collection and the +// given map. Keys already present in the collection are not overwritten. +func (m *Collection[K, V]) Union(items map[K]V) *Collection[K, V] { + result := make(map[K]V, len(m.items)+len(items)) + newKeys := make([]K, len(m.keys)) + copy(newKeys, m.keys) + + for k, v := range m.items { + result[k] = v + } + + for k, v := range items { + if _, exists := result[k]; !exists { + result[k] = v + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// Replace returns a new collection with the given map merged in. This is an +// alias for Merge. +func (m *Collection[K, V]) Replace(items map[K]V) *Collection[K, V] { + return m.Merge(items) +} + +// MergeRecursive recursively merges the given map into the collection. +// When both sides have a map value for the same key, they are merged recursively. +func MergeRecursive[V any](m *Collection[string, V], items map[string]V) *Collection[string, V] { + return m.Merge(items) +} + +// DiffKeys returns a new collection containing items whose keys are not +// present in the given map. +func (m *Collection[K, V]) DiffKeys(items map[K]V) *Collection[K, V] { + result := make(map[K]V) + newKeys := make([]K, 0) + + for _, k := range m.keys { + if _, exists := items[k]; !exists { + result[k] = m.items[k] + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// DiffKeysUsing returns items whose keys are not considered equal to any key in the given map, +// using the provided comparison function. +func (m *Collection[K, V]) DiffKeysUsing(items map[K]V, equals func(K, K) bool) *Collection[K, V] { + result := make(map[K]V) + newKeys := make([]K, 0) + + for _, k := range m.keys { + found := false + + for otherK := range items { + if equals(k, otherK) { + found = true + + break + } + } + + if !found { + result[k] = m.items[k] + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// IntersectByKeys returns a new collection containing items whose keys are +// present in the given map. +func (m *Collection[K, V]) IntersectByKeys(items map[K]V) *Collection[K, V] { + result := make(map[K]V) + newKeys := make([]K, 0) + + for _, k := range m.keys { + if _, exists := items[k]; exists { + result[k] = m.items[k] + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// DiffAssoc returns items whose key is not present in the given map or whose value differs. +func DiffAssoc[K comparable, V comparable](m *Collection[K, V], items map[K]V) *Collection[K, V] { + result := make(map[K]V) + newKeys := make([]K, 0) + + for _, k := range m.keys { + otherVal, exists := items[k] + + if !exists || m.items[k] != otherVal { + result[k] = m.items[k] + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// IntersectAssoc returns items whose key exists in the given map and whose value matches. +func IntersectAssoc[K comparable, V comparable](m *Collection[K, V], items map[K]V) *Collection[K, V] { + result := make(map[K]V) + newKeys := make([]K, 0) + + for _, k := range m.keys { + if otherVal, exists := items[k]; exists && m.items[k] == otherVal { + result[k] = m.items[k] + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// Flip swaps keys and values, returning a new Collection where the +// original values become keys and original keys become values. +func Flip[K comparable, V comparable](m *Collection[K, V]) *Collection[V, K] { + result := make(map[V]K, len(m.items)) + newKeys := make([]V, 0, len(m.keys)) + + for _, k := range m.keys { + v := m.items[k] + + if _, exists := result[v]; !exists { + newKeys = append(newKeys, v) + } + + result[v] = k + } + + return &Collection[V, K]{items: result, keys: newKeys} +} diff --git a/collectible/doc.go b/collectible/doc.go new file mode 100644 index 0000000..505e5cd --- /dev/null +++ b/collectible/doc.go @@ -0,0 +1,10 @@ +// Package collectible provides an ordered map collection with a fluent, +// chainable API. +// +// [Collection] wraps a Go map while preserving insertion order. Unlike +// Go's built-in map, iterating over a Collection always visits entries +// in the order they were added. +// +// Methods that extract keys or values return plain slices, keeping the +// package independent of other collection packages. +package collectible diff --git a/collectible/mutation.go b/collectible/mutation.go new file mode 100644 index 0000000..c4777ce --- /dev/null +++ b/collectible/mutation.go @@ -0,0 +1,53 @@ +package collectible + +// GetOrPut returns the value for the given key if it exists. Otherwise, it +// stores and returns the provided default value. +func (m *Collection[K, V]) GetOrPut(key K, value V) V { + if v, ok := m.items[key]; ok { + return v + } + + m.Put(key, value) + + return value +} + +// Put sets the given key-value pair in the collection. +func (m *Collection[K, V]) Put(key K, value V) *Collection[K, V] { + if _, exists := m.items[key]; !exists { + m.keys = append(m.keys, key) + } + + m.items[key] = value + + return m +} + +// Pull removes an item by key and returns its value. The second return value +// indicates whether the key was found. +func (m *Collection[K, V]) Pull(key K) (V, bool) { + v, ok := m.items[key] + + if ok { + delete(m.items, key) + + for i, k := range m.keys { + if k == key { + m.keys = append(m.keys[:i], m.keys[i+1:]...) + + break + } + } + } + + return v, ok +} + +// Forget removes one or more items by key. +func (m *Collection[K, V]) Forget(keys ...K) *Collection[K, V] { + for _, key := range keys { + m.Pull(key) + } + + return m +} diff --git a/collectible/pipeline.go b/collectible/pipeline.go new file mode 100644 index 0000000..0ed636a --- /dev/null +++ b/collectible/pipeline.go @@ -0,0 +1,136 @@ +package collectible + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/gocanto/collection/arr" + "github.com/gocanto/collection/support" +) + +// Each iterates over items in insertion order, calling the callback for each +// key-value pair. Return false from the callback to stop iteration. +func (m *Collection[K, V]) Each(callback func(V, K) bool) *Collection[K, V] { + for _, k := range m.keys { + if !callback(m.items[k], k) { + break + } + } + + return m +} + +// Implode concatenates all values into a single string, separated by the +// given glue string. +func (m *Collection[K, V]) Implode(glue string) string { + parts := make([]string, 0, len(m.keys)) + + for _, k := range m.keys { + parts = append(parts, fmt.Sprint(m.items[k])) + } + + return strings.Join(parts, glue) +} + +// Join concatenates all values into a string separated by glue. Optional +// final glue is placed between the last two items. +func (m *Collection[K, V]) Join(glue string, finalGlues ...string) string { + parts := make([]string, 0, len(m.keys)) + + for _, k := range m.keys { + parts = append(parts, fmt.Sprint(m.items[k])) + } + + return arr.Join(parts, glue, finalGlues...) +} + +// Tap passes the collection to the callback and returns the collection, +// allowing side effects without breaking a method chain. +func (m *Collection[K, V]) Tap(callback func(*Collection[K, V])) *Collection[K, V] { + callback(m) + + return m +} + +// When applies the callback if the condition is true. An optional default +// callback is applied when the condition is false. +func (m *Collection[K, V]) When(condition bool, callback func(*Collection[K, V]) *Collection[K, V], defaults ...func(*Collection[K, V]) *Collection[K, V]) *Collection[K, V] { + if condition { + return callback(m) + } + + if len(defaults) > 0 { + return defaults[0](m) + } + + return m +} + +// Unless applies the callback unless the condition is true. An optional +// default callback is applied when the condition is true. +func (m *Collection[K, V]) Unless(condition bool, callback func(*Collection[K, V]) *Collection[K, V], defaults ...func(*Collection[K, V]) *Collection[K, V]) *Collection[K, V] { + return m.When(!condition, callback, defaults...) +} + +// ToJSON serializes the collection to JSON. +func (m *Collection[K, V]) ToJSON() ([]byte, error) { + return json.Marshal(m.items) +} + +// ToPrettyJSON serializes the collection to indented JSON. +func (m *Collection[K, V]) ToPrettyJSON() ([]byte, error) { + return json.MarshalIndent(m.items, "", " ") +} + +// String returns the JSON representation of the collection. +func (m *Collection[K, V]) String() string { + b, err := m.ToJSON() + + if err != nil { + return "{}" + } + + return string(b) +} + +// MarshalJSON implements the json.Marshaler interface. +func (m *Collection[K, V]) MarshalJSON() ([]byte, error) { + return json.Marshal(m.items) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (m *Collection[K, V]) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.items) +} + +// Copy creates a shallow copy of the collection. +func (m *Collection[K, V]) Copy() *Collection[K, V] { + return New(m.All()) +} + +// Dump prints the underlying map to stdout for debugging purposes. +func (m *Collection[K, V]) Dump() *Collection[K, V] { + fmt.Printf("%v\n", m.items) + + return m +} + +// DD prints the map collection for debugging and terminates the program. +func (m *Collection[K, V]) DD() { + m.Dump() + os.Exit(1) +} + +// ToPairs converts the collection to a slice of Pair values, preserving +// insertion order. +func (m *Collection[K, V]) ToPairs() []support.Pair[K, V] { + result := make([]support.Pair[K, V], 0, len(m.keys)) + + for _, k := range m.keys { + result = append(result, support.Pair[K, V]{Key: k, Value: m.items[k]}) + } + + return result +} diff --git a/collectible/sorting.go b/collectible/sorting.go new file mode 100644 index 0000000..bc8b24c --- /dev/null +++ b/collectible/sorting.go @@ -0,0 +1,56 @@ +package collectible + +import "sort" + +// SortKeys returns a new collection with string keys sorted in ascending +// order. +func SortKeys[V any](m *Collection[string, V]) *Collection[string, V] { + result := make(map[string]V, len(m.items)) + + for k, v := range m.items { + result[k] = v + } + + newKeys := make([]string, len(m.keys)) + copy(newKeys, m.keys) + + sort.Strings(newKeys) + + return &Collection[string, V]{items: result, keys: newKeys} +} + +// SortKeysDesc returns a new collection with string keys sorted in +// descending order. +func SortKeysDesc[V any](m *Collection[string, V]) *Collection[string, V] { + result := make(map[string]V, len(m.items)) + + for k, v := range m.items { + result[k] = v + } + + newKeys := make([]string, len(m.keys)) + copy(newKeys, m.keys) + + sort.Sort(sort.Reverse(sort.StringSlice(newKeys))) + + return &Collection[string, V]{items: result, keys: newKeys} +} + +// SortKeysUsing returns a new collection with keys sorted using the provided +// comparison function. +func (m *Collection[K, V]) SortKeysUsing(less func(K, K) bool) *Collection[K, V] { + result := make(map[K]V, len(m.items)) + + for k, v := range m.items { + result[k] = v + } + + newKeys := make([]K, len(m.keys)) + copy(newKeys, m.keys) + + sort.SliceStable(newKeys, func(i, j int) bool { + return less(newKeys[i], newKeys[j]) + }) + + return &Collection[K, V]{items: result, keys: newKeys} +} diff --git a/collectible/transform.go b/collectible/transform.go new file mode 100644 index 0000000..c4b1bc2 --- /dev/null +++ b/collectible/transform.go @@ -0,0 +1,113 @@ +package collectible + +// Only returns a new collection containing only the specified keys. +func (m *Collection[K, V]) Only(keys ...K) *Collection[K, V] { + result := make(map[K]V) + newKeys := make([]K, 0, len(keys)) + + for _, k := range keys { + if v, ok := m.items[k]; ok { + result[k] = v + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// Except returns a new collection containing all items except those with the +// specified keys. +func (m *Collection[K, V]) Except(keys ...K) *Collection[K, V] { + excludeSet := make(map[K]bool, len(keys)) + + for _, k := range keys { + excludeSet[k] = true + } + + result := make(map[K]V) + newKeys := make([]K, 0) + + for _, k := range m.keys { + if !excludeSet[k] { + result[k] = m.items[k] + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// Filter returns a new collection containing only the items for which the +// callback returns true. +func (m *Collection[K, V]) Filter(callback func(V, K) bool) *Collection[K, V] { + result := make(map[K]V) + newKeys := make([]K, 0) + + for _, k := range m.keys { + v := m.items[k] + + if callback(v, k) { + result[k] = v + newKeys = append(newKeys, k) + } + } + + return &Collection[K, V]{items: result, keys: newKeys} +} + +// Reject returns a new collection containing only the items for which the +// callback returns false. +func (m *Collection[K, V]) Reject(callback func(V, K) bool) *Collection[K, V] { + return m.Filter(func(v V, k K) bool { + return !callback(v, k) + }) +} + +// MapValues transforms each value using the callback, returning a new +// Collection with the same keys and transformed values. +func MapValues[K comparable, V any, R any](m *Collection[K, V], callback func(V, K) R) *Collection[K, R] { + result := make(map[K]R, len(m.items)) + newKeys := make([]K, len(m.keys)) + copy(newKeys, m.keys) + + for _, k := range m.keys { + result[k] = callback(m.items[k], k) + } + + return &Collection[K, R]{items: result, keys: newKeys} +} + +// Every report whether all items in the collection satisfy the callback. +func (m *Collection[K, V]) Every(callback func(V, K) bool) bool { + for _, k := range m.keys { + if !callback(m.items[k], k) { + return false + } + } + + return true +} + +// Partition splits the collection into two: one where the callback returns +// true, and one where it returns false. +func (m *Collection[K, V]) Partition(callback func(V, K) bool) (*Collection[K, V], *Collection[K, V]) { + pass := make(map[K]V) + passKeys := make([]K, 0) + fail := make(map[K]V) + failKeys := make([]K, 0) + + for _, k := range m.keys { + v := m.items[k] + + if callback(v, k) { + pass[k] = v + passKeys = append(passKeys, k) + } else { + fail[k] = v + failKeys = append(failKeys, k) + } + } + + return &Collection[K, V]{items: pass, keys: passKeys}, + &Collection[K, V]{items: fail, keys: failKeys} +} diff --git a/collection/access.go b/collection/access.go new file mode 100644 index 0000000..83aa407 --- /dev/null +++ b/collection/access.go @@ -0,0 +1,280 @@ +package collection + +import "github.com/gocanto/collection/support" + +// First returns the first element matching the optional predicate. +// If no predicate is provided, the first element is returned. +// The second return value indicates whether a match was found. +func (c *Collection[T]) First(predicates ...func(T, int) bool) (T, bool) { + if len(c.items) == 0 { + var zero T + + return zero, false + } + + if len(predicates) == 0 || predicates[0] == nil { + return c.items[0], true + } + + predicate := predicates[0] + + for i, item := range c.items { + if predicate(item, i) { + return item, true + } + } + + var zero T + + return zero, false +} + +// FirstOrFail returns the first element matching the optional predicate, +// or an ItemNotFoundError if no match is found. +func (c *Collection[T]) FirstOrFail(predicates ...func(T, int) bool) (T, error) { + item, ok := c.First(predicates...) + + if !ok { + var zero T + + return zero, &support.ItemNotFoundError{} + } + + return item, nil +} + +// Last returns the last element matching the optional predicate. +// If no predicate is provided, the last element is returned. +// The second return value indicates whether a match was found. +func (c *Collection[T]) Last(predicates ...func(T, int) bool) (T, bool) { + if len(c.items) == 0 { + var zero T + + return zero, false + } + + if len(predicates) == 0 || predicates[0] == nil { + return c.items[len(c.items)-1], true + } + + predicate := predicates[0] + + for i := len(c.items) - 1; i >= 0; i-- { + if predicate(c.items[i], i) { + return c.items[i], true + } + } + + var zero T + + return zero, false +} + +// Sole returns the only element matching the optional predicate. +// It returns an ItemNotFoundError if no items match, or a MultipleItemsFoundError +// if more than one item matches. +func (c *Collection[T]) Sole(predicates ...func(T, int) bool) (T, error) { + var filtered *Collection[T] + + if len(predicates) == 0 || predicates[0] == nil { + filtered = c + } else { + filtered = c.Filter(predicates[0]) + } + + if filtered.Count() == 0 { + var zero T + + return zero, &support.ItemNotFoundError{} + } + + if filtered.Count() > 1 { + var zero T + + return zero, &support.MultipleItemsFoundError{Count: filtered.Count()} + } + + return filtered.items[0], nil +} + +// HasSole reports whether exactly one item matches the optional predicate. +func (c *Collection[T]) HasSole(predicates ...func(T, int) bool) bool { + var filtered *Collection[T] + + if len(predicates) == 0 || predicates[0] == nil { + filtered = c + } else { + filtered = c.Filter(predicates[0]) + } + + return filtered.Count() == 1 +} + +// Get returns the item at the given index. +// Negative indices count from the end. The second return value indicates +// whether the index was within bounds. An optional default may be provided. +func (c *Collection[T]) Get(index int, defaults ...T) (T, bool) { + if index < 0 { + index = len(c.items) + index + } + + if index >= 0 && index < len(c.items) { + return c.items[index], true + } + + if len(defaults) > 0 { + return defaults[0], false + } + + var zero T + + return zero, false +} + +// GetOrPut returns the item at the given index if it exists. +// Otherwise, it appends the value to the collection and returns it. +func (c *Collection[T]) GetOrPut(index int, value T) T { + if index >= 0 && index < len(c.items) { + return c.items[index] + } + + c.items = append(c.items, value) + + return value +} + +// Contains reports whether any item in the collection satisfies the predicate. +func (c *Collection[T]) Contains(predicate func(T, int) bool) bool { + for i, item := range c.items { + if predicate(item, i) { + return true + } + } + + return false +} + +// Some is an alias for Contains. +func (c *Collection[T]) Some(predicate func(T, int) bool) bool { + return c.Contains(predicate) +} + +// DoesntContain reports whether no item in the collection satisfies the predicate. +func (c *Collection[T]) DoesntContain(predicate func(T, int) bool) bool { + return !c.Contains(predicate) +} + +// Search returns the index of the first item satisfying the predicate. +// The second return value indicates whether a match was found. +func (c *Collection[T]) Search(predicate func(T, int) bool) (int, bool) { + for i, item := range c.items { + if predicate(item, i) { + return i, true + } + } + + return -1, false +} + +// Before returns the item immediately before the first item matching the predicate. +func (c *Collection[T]) Before(predicate func(T, int) bool) (T, bool) { + for i, item := range c.items { + if predicate(item, i) { + if i == 0 { + var zero T + + return zero, false + } + + return c.items[i-1], true + } + } + + var zero T + + return zero, false +} + +// After returns the item immediately after the first item matching the predicate. +func (c *Collection[T]) After(predicate func(T, int) bool) (T, bool) { + for i, item := range c.items { + if predicate(item, i) { + if i >= len(c.items)-1 { + var zero T + + return zero, false + } + + return c.items[i+1], true + } + } + + var zero T + + return zero, false +} + +// Has reports whether the given index exists in the collection. +// Negative indices count from the end. +func (c *Collection[T]) Has(index int) bool { + if index < 0 { + index = len(c.items) + index + } + + return index >= 0 && index < len(c.items) +} + +// HasAny reports whether any of the given indices exist in the collection. +func (c *Collection[T]) HasAny(indices ...int) bool { + for _, idx := range indices { + if c.Has(idx) { + return true + } + } + + return false +} + +// Only returns a new collection containing only items at the given indices. +func (c *Collection[T]) Only(indices ...int) *Collection[T] { + result := make([]T, 0, len(indices)) + + for _, idx := range indices { + if idx >= 0 && idx < len(c.items) { + result = append(result, c.items[idx]) + } + } + + return Collect(result) +} + +// Except returns a new collection excluding items at the given indices. +func (c *Collection[T]) Except(indices ...int) *Collection[T] { + excludeSet := make(map[int]bool, len(indices)) + + for _, idx := range indices { + excludeSet[idx] = true + } + + result := make([]T, 0) + + for i, item := range c.items { + if !excludeSet[i] { + result = append(result, item) + } + } + + return Collect(result) +} + +// Dot returns a shallow copy of the collection. +// For typed Go slices, no dot-notation expansion is possible. +func (c *Collection[T]) Dot() *Collection[T] { + return c.Copy() +} + +// Undot returns a shallow copy of the collection. +// For typed Go slices, no dot-notation expansion is possible. +func (c *Collection[T]) Undot() *Collection[T] { + return c.Copy() +} diff --git a/collection/aggregate.go b/collection/aggregate.go new file mode 100644 index 0000000..1e8741b --- /dev/null +++ b/collection/aggregate.go @@ -0,0 +1,196 @@ +package collection + +import ( + "cmp" + "slices" + + "github.com/gocanto/collection/support" +) + +// Median returns the median value of a float64 collection. +func Median(c *Collection[float64]) float64 { + if len(c.items) == 0 { + return 0 + } + + sorted := make([]float64, len(c.items)) + copy(sorted, c.items) + + slices.Sort(sorted) + + mid := len(sorted) / 2 + + if len(sorted)%2 == 0 { + return (sorted[mid-1] + sorted[mid]) / 2 + } + + return sorted[mid] +} + +// MedianBy returns the median value extracted from each item by the given function. +func MedianBy[T any](c *Collection[T], valueFunc func(T) float64) float64 { + return Median(Map(c, func(item T, _ int) float64 { + return valueFunc(item) + })) +} + +// Mode returns the most frequently occurring values in the collection. +func Mode[T comparable](c *Collection[T]) []T { + if len(c.items) == 0 { + return nil + } + + counts := make(map[T]int) + maxCount := 0 + + for _, item := range c.items { + counts[item]++ + + if counts[item] > maxCount { + maxCount = counts[item] + } + } + + result := make([]T, 0) + + for item, count := range counts { + if count == maxCount { + result = append(result, item) + } + } + + return result +} + +// Sum returns the sum of all items in a numeric collection. +func Sum[T support.Numeric](c *Collection[T]) T { + var total T + + for _, item := range c.items { + total += item + } + + return total +} + +// SumBy returns the sum of values extracted from each item by the given function. +func SumBy[T any, N support.Numeric](c *Collection[T], valueFunc func(T) N) N { + var total N + + for _, item := range c.items { + total += valueFunc(item) + } + + return total +} + +// Avg returns the arithmetic mean of all items in a numeric collection. +func Avg[T support.Numeric](c *Collection[T]) float64 { + if len(c.items) == 0 { + return 0 + } + + return float64(Sum(c)) / float64(len(c.items)) +} + +// AvgBy returns the arithmetic mean of values extracted from each item by the given function. +func AvgBy[T any, N support.Numeric](c *Collection[T], valueFunc func(T) N) float64 { + if len(c.items) == 0 { + return 0 + } + + return float64(SumBy(c, valueFunc)) / float64(len(c.items)) +} + +// Average is an alias for Avg. +func Average[T support.Numeric](c *Collection[T]) float64 { + return Avg(c) +} + +// Min returns the minimum value in an ordered collection. +// The second return value indicates whether the collection was non-empty. +func Min[T cmp.Ordered](c *Collection[T]) (T, bool) { + if len(c.items) == 0 { + var zero T + + return zero, false + } + + result := c.items[0] + + for _, item := range c.items[1:] { + if item < result { + result = item + } + } + + return result, true +} + +// MinBy returns the item with the minimum key as determined by the given function. +// The second return value indicates whether the collection was non-empty. +func MinBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) (T, bool) { + if len(c.items) == 0 { + var zero T + + return zero, false + } + + result := c.items[0] + minKey := keyFunc(result) + + for _, item := range c.items[1:] { + k := keyFunc(item) + + if k < minKey { + minKey = k + result = item + } + } + + return result, true +} + +// Max returns the maximum value in an ordered collection. +// The second return value indicates whether the collection was non-empty. +func Max[T cmp.Ordered](c *Collection[T]) (T, bool) { + if len(c.items) == 0 { + var zero T + + return zero, false + } + + result := c.items[0] + + for _, item := range c.items[1:] { + if item > result { + result = item + } + } + + return result, true +} + +// MaxBy returns the item with the maximum key as determined by the given function. +// The second return value indicates whether the collection was non-empty. +func MaxBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) (T, bool) { + if len(c.items) == 0 { + var zero T + + return zero, false + } + + result := c.items[0] + maxKey := keyFunc(result) + + for _, item := range c.items[1:] { + k := keyFunc(item) + + if k > maxKey { + maxKey = k + result = item + } + } + + return result, true +} diff --git a/collection/collection.go b/collection/collection.go index fd52ae1..4363bba 100644 --- a/collection/collection.go +++ b/collection/collection.go @@ -1,18 +1,7 @@ // Package collection provides a fluent, generic wrapper for working with slices of data. package collection -import ( - "cmp" - "encoding/json" - "fmt" - "iter" - "math" - "math/rand/v2" - "os" - "slices" - "sort" - "strings" -) +import "iter" // Collection wraps a slice and provides a fluent API for working with arrays of data. type Collection[T any] struct { @@ -24,6 +13,7 @@ func New[T any](items ...T) *Collection[T] { if items == nil { items = make([]T, 0) } + return &Collection[T]{items: items} } @@ -32,6 +22,7 @@ func Collect[T any](items []T) *Collection[T] { if items == nil { items = make([]T, 0) } + return &Collection[T]{items: items} } @@ -51,6 +42,7 @@ func Wrap[T any](value any) *Collection[T] { if item, ok := value.(T); ok { return New(item) } + return Empty[T]() } } @@ -60,21 +52,26 @@ func Unwrap[T any](value any) []T { if c, ok := value.(*Collection[T]); ok { return c.All() } + if items, ok := value.([]T); ok { return items } + return nil } -// Times creates a new collection by invoking the callback a given number of times. +// Times create a new collection by invoking the callback a given number of times. func Times[T any](number int, callback func(int) T) *Collection[T] { if number < 1 { return Empty[T]() } + items := make([]T, number) + for i := 0; i < number; i++ { items[i] = callback(i + 1) } + return Collect(items) } @@ -82,15 +79,20 @@ func Times[T any](number int, callback func(int) T) *Collection[T] { func Range(from, to int) *Collection[int] { if from > to { items := make([]int, 0, from-to+1) + for i := from; i >= to; i-- { items = append(items, i) } + return Collect(items) } + items := make([]int, 0, to-from+1) + for i := from; i <= to; i++ { items = append(items, i) } + return Collect(items) } @@ -129,1497 +131,16 @@ func (c *Collection[T]) HasMany() bool { return c.ContainsManyItems() } -// First returns the first element matching the optional predicate. -// If no predicate is provided, the first element is returned. -// The second return value indicates whether a match was found. -func (c *Collection[T]) First(predicates ...func(T, int) bool) (T, bool) { - if len(c.items) == 0 { - var zero T - return zero, false - } - if len(predicates) == 0 || predicates[0] == nil { - return c.items[0], true - } - predicate := predicates[0] - for i, item := range c.items { - if predicate(item, i) { - return item, true - } - } - var zero T - return zero, false -} - -// FirstOrFail returns the first element matching the optional predicate, -// or an ItemNotFoundError if no match is found. -func (c *Collection[T]) FirstOrFail(predicates ...func(T, int) bool) (T, error) { - item, ok := c.First(predicates...) - if !ok { - var zero T - return zero, &ItemNotFoundError{} - } - return item, nil -} - -// Last returns the last element matching the optional predicate. -// If no predicate is provided, the last element is returned. -// The second return value indicates whether a match was found. -func (c *Collection[T]) Last(predicates ...func(T, int) bool) (T, bool) { - if len(c.items) == 0 { - var zero T - return zero, false - } - if len(predicates) == 0 || predicates[0] == nil { - return c.items[len(c.items)-1], true - } - predicate := predicates[0] - for i := len(c.items) - 1; i >= 0; i-- { - if predicate(c.items[i], i) { - return c.items[i], true - } - } - var zero T - return zero, false -} - -// Sole returns the only element matching the optional predicate. -// It returns an ItemNotFoundError if no items match, or a MultipleItemsFoundError -// if more than one item matches. -func (c *Collection[T]) Sole(predicates ...func(T, int) bool) (T, error) { - var filtered *Collection[T] - if len(predicates) == 0 || predicates[0] == nil { - filtered = c - } else { - filtered = c.Filter(predicates[0]) - } - - if filtered.Count() == 0 { - var zero T - return zero, &ItemNotFoundError{} - } - if filtered.Count() > 1 { - var zero T - return zero, &MultipleItemsFoundError{Count: filtered.Count()} - } - return filtered.items[0], nil -} - -// HasSole reports whether exactly one item matches the optional predicate. -func (c *Collection[T]) HasSole(predicates ...func(T, int) bool) bool { - var filtered *Collection[T] - if len(predicates) == 0 || predicates[0] == nil { - filtered = c - } else { - filtered = c.Filter(predicates[0]) - } - return filtered.Count() == 1 -} - -// Get returns the item at the given index. -// Negative indices count from the end. The second return value indicates -// whether the index was within bounds. An optional default may be provided. -func (c *Collection[T]) Get(index int, defaults ...T) (T, bool) { - if index < 0 { - index = len(c.items) + index - } - if index >= 0 && index < len(c.items) { - return c.items[index], true - } - if len(defaults) > 0 { - return defaults[0], false - } - var zero T - return zero, false -} - -// GetOrPut returns the item at the given index if it exists. -// Otherwise it appends the value to the collection and returns it. -func (c *Collection[T]) GetOrPut(index int, value T) T { - if index >= 0 && index < len(c.items) { - return c.items[index] - } - c.items = append(c.items, value) - return value -} - -// Put sets the item at the given index to the given value. -func (c *Collection[T]) Put(index int, value T) *Collection[T] { - if index >= 0 && index < len(c.items) { - c.items[index] = value - } - return c -} - -// Pull removes and returns the item at the given index. -// The second return value indicates whether the index was valid. -func (c *Collection[T]) Pull(index int) (T, bool) { - if index < 0 || index >= len(c.items) { - var zero T - return zero, false - } - item := c.items[index] - c.items = append(c.items[:index], c.items[index+1:]...) - return item, true -} - -// Contains reports whether any item in the collection satisfies the predicate. -func (c *Collection[T]) Contains(predicate func(T, int) bool) bool { - for i, item := range c.items { - if predicate(item, i) { - return true - } - } - return false -} - -// Some is an alias for Contains. -func (c *Collection[T]) Some(predicate func(T, int) bool) bool { - return c.Contains(predicate) -} - -// DoesntContain reports whether no item in the collection satisfies the predicate. -func (c *Collection[T]) DoesntContain(predicate func(T, int) bool) bool { - return !c.Contains(predicate) -} - -// Search returns the index of the first item satisfying the predicate. -// The second return value indicates whether a match was found. -func (c *Collection[T]) Search(predicate func(T, int) bool) (int, bool) { - for i, item := range c.items { - if predicate(item, i) { - return i, true - } - } - return -1, false -} - -// Before returns the item immediately before the first item matching the predicate. -func (c *Collection[T]) Before(predicate func(T, int) bool) (T, bool) { - for i, item := range c.items { - if predicate(item, i) { - if i == 0 { - var zero T - return zero, false - } - return c.items[i-1], true - } - } - var zero T - return zero, false -} - -// After returns the item immediately after the first item matching the predicate. -func (c *Collection[T]) After(predicate func(T, int) bool) (T, bool) { - for i, item := range c.items { - if predicate(item, i) { - if i >= len(c.items)-1 { - var zero T - return zero, false - } - return c.items[i+1], true - } - } - var zero T - return zero, false -} - -// Push appends one or more items to the end of the collection. -func (c *Collection[T]) Push(values ...T) *Collection[T] { - c.items = append(c.items, values...) - return c -} - -// Add appends a single item to the end of the collection. -func (c *Collection[T]) Add(item T) *Collection[T] { - return c.Push(item) -} - -// Prepend adds an item to the beginning of the collection. -func (c *Collection[T]) Prepend(value T) *Collection[T] { - c.items = append([]T{value}, c.items...) - return c -} - -// Unshift is an alias for Prepend. -func (c *Collection[T]) Unshift(value T) *Collection[T] { - return c.Prepend(value) -} - -// Pop removes and returns the last item from the collection. -// The second return value indicates whether the collection was non-empty. -func (c *Collection[T]) Pop(counts ...int) (T, bool) { - if len(c.items) == 0 { - var zero T - return zero, false - } - count := 1 - if len(counts) > 0 { - count = counts[0] - } - _ = count // For single pop - item := c.items[len(c.items)-1] - c.items = c.items[:len(c.items)-1] - return item, true -} - -// PopMany removes and returns the last n items from the collection. -func (c *Collection[T]) PopMany(count int) *Collection[T] { - if count >= len(c.items) { - popped := Collect(c.items) - c.items = make([]T, 0) - return popped - } - idx := len(c.items) - count - popped := Collect(c.items[idx:]) - c.items = c.items[:idx] - return popped -} - -// Shift removes and returns the first item from the collection. -// The second return value indicates whether the collection was non-empty. -func (c *Collection[T]) Shift() (T, bool) { - if len(c.items) == 0 { - var zero T - return zero, false - } - item := c.items[0] - c.items = c.items[1:] - return item, true -} - -// ShiftMany removes and returns the first n items from the collection. -func (c *Collection[T]) ShiftMany(count int) *Collection[T] { - if count >= len(c.items) { - shifted := Collect(c.items) - c.items = make([]T, 0) - return shifted - } - shifted := Collect(c.items[:count]) - c.items = c.items[count:] - return shifted -} - -// Each iterates over the items, passing each item and its index to the callback. -// Return false from the callback to stop iterating. -func (c *Collection[T]) Each(callback func(T, int) bool) *Collection[T] { - for i, item := range c.items { - if !callback(item, i) { - break - } - } - return c -} - -// EachSpread iterates over the collection's items, passing each item value -// into the given callback. In Go this operates the same as Each. -func (c *Collection[T]) EachSpread(callback func(T, int) bool) *Collection[T] { - return c.Each(callback) -} - -// Tap passes the collection to the given callback and returns the collection unchanged. -func (c *Collection[T]) Tap(callback func(*Collection[T])) *Collection[T] { - callback(c) - return c -} - -// Pipe passes the collection to the given callback and returns the callback's result. -func Pipe[T any, R any](c *Collection[T], callback func(*Collection[T]) R) R { - return callback(c) -} - -// PipeInto passes the collection to the given constructor and returns the result. -func PipeInto[T any, R any](c *Collection[T], constructor func(*Collection[T]) R) R { - return constructor(c) -} - -// PipeThrough passes the collection through a series of callbacks, returning the final result. -func PipeThrough[T any](c *Collection[T], callbacks ...func(*Collection[T]) *Collection[T]) *Collection[T] { - result := c - for _, cb := range callbacks { - result = cb(result) - } - return result -} - -// Filter returns a new collection containing only items for which the callback returns true. -func (c *Collection[T]) Filter(callback func(T, int) bool) *Collection[T] { - result := make([]T, 0) - for i, item := range c.items { - if callback(item, i) { - result = append(result, item) - } - } - return Collect(result) -} - -// Reject returns a new collection containing only items for which the callback returns false. -func (c *Collection[T]) Reject(callback func(T, int) bool) *Collection[T] { - return c.Filter(func(item T, index int) bool { - return !callback(item, index) - }) -} - -// Map applies the callback to each item and returns a new collection of results. -func Map[T any, R any](c *Collection[T], callback func(T, int) R) *Collection[R] { - result := make([]R, len(c.items)) - for i, item := range c.items { - result[i] = callback(item, i) - } - return Collect(result) -} - -// Transform applies the callback to each item in place, mutating the collection. -func (c *Collection[T]) Transform(callback func(T, int) T) *Collection[T] { - for i, item := range c.items { - c.items[i] = callback(item, i) - } - return c -} - -// FlatMap applies the callback to each item, flattening the resulting slices into a single collection. -func FlatMap[T any, R any](c *Collection[T], callback func(T, int) []R) *Collection[R] { - result := make([]R, 0) - for i, item := range c.items { - result = append(result, callback(item, i)...) - } - return Collect(result) -} - -// MapInto applies the constructor to each item, returning a new collection of the mapped type. -func MapInto[T any, R any](c *Collection[T], constructor func(T) R) *Collection[R] { - result := make([]R, len(c.items)) - for i, item := range c.items { - result[i] = constructor(item) - } - return Collect(result) -} - -// Reduce iterates over the collection and accumulates a single result using the callback. -func Reduce[T any, R any](c *Collection[T], callback func(R, T, int) R, initial R) R { - result := initial - for i, item := range c.items { - result = callback(result, item, i) - } - return result -} - -// Flatten returns a shallow copy of the collection. -// For non-nested typed slices this returns the items as-is. -func (c *Collection[T]) Flatten() *Collection[T] { - return Collect(append([]T{}, c.items...)) -} - -// Chunk breaks the collection into multiple slices of the given size. -func (c *Collection[T]) Chunk(size int) [][]T { - if size <= 0 { - return nil - } - chunks := make([][]T, 0) - for i := 0; i < len(c.items); i += size { - end := i + size - if end > len(c.items) { - end = len(c.items) - } - chunk := make([]T, end-i) - copy(chunk, c.items[i:end]) - chunks = append(chunks, chunk) - } - return chunks -} - -// ChunkWhile breaks the collection into groups as long as the callback returns true. -// A new group is started each time the callback returns false. -func (c *Collection[T]) ChunkWhile(callback func(T, int, []T) bool) [][]T { - if len(c.items) == 0 { - return nil - } - chunks := make([][]T, 0) - current := []T{c.items[0]} - for i := 1; i < len(c.items); i++ { - if callback(c.items[i], i, current) { - current = append(current, c.items[i]) - } else { - chunks = append(chunks, current) - current = []T{c.items[i]} - } - } - chunks = append(chunks, current) - return chunks -} - -// Split breaks the collection into the given number of groups. -func (c *Collection[T]) Split(numberOfGroups int) [][]T { - if len(c.items) == 0 || numberOfGroups <= 0 { - return nil - } - groups := make([][]T, 0, numberOfGroups) - groupSize := float64(len(c.items)) / float64(numberOfGroups) - for i := 0; i < numberOfGroups; i++ { - start := int(math.Round(float64(i) * groupSize)) - end := int(math.Round(float64(i+1) * groupSize)) - if start >= len(c.items) { - break - } - if end > len(c.items) { - end = len(c.items) - } - chunk := make([]T, end-start) - copy(chunk, c.items[start:end]) - groups = append(groups, chunk) - } - return groups -} - -// SplitIn splits the collection into groups, filling non-terminal groups completely. -func (c *Collection[T]) SplitIn(numberOfGroups int) [][]T { - size := int(math.Ceil(float64(len(c.items)) / float64(numberOfGroups))) - return c.Chunk(size) -} - -// Sliding returns a sliding window view of the collection with the given window size and step. -func (c *Collection[T]) Sliding(size int, steps ...int) [][]T { - step := 1 - if len(steps) > 0 { - step = steps[0] - } - if size <= 0 || step <= 0 || len(c.items) == 0 { - return nil - } - chunks := make([][]T, 0) - for i := 0; i+size <= len(c.items); i += step { - chunk := make([]T, size) - copy(chunk, c.items[i:i+size]) - chunks = append(chunks, chunk) - } - return chunks -} - -// Slice extracts a portion of the collection starting at the given offset. -// An optional length limits how many items are returned. -func (c *Collection[T]) Slice(offset int, lengths ...int) *Collection[T] { - if offset < 0 { - offset = len(c.items) + offset - if offset < 0 { - offset = 0 - } - } - if offset >= len(c.items) { - return Empty[T]() - } - if len(lengths) > 0 { - end := offset + lengths[0] - if end > len(c.items) { - end = len(c.items) - } - result := make([]T, end-offset) - copy(result, c.items[offset:end]) - return Collect(result) - } - result := make([]T, len(c.items)-offset) - copy(result, c.items[offset:]) - return Collect(result) -} - -// Splice removes and returns a slice of items starting at the given offset. -// An optional length limits the number of items removed. -func (c *Collection[T]) Splice(offset int, lengths ...int) *Collection[T] { - length := len(c.items) - offset - if len(lengths) > 0 { - length = lengths[0] - } - if offset < 0 { - offset = len(c.items) + offset - if offset < 0 { - offset = 0 - } - } - if offset >= len(c.items) { - return Empty[T]() - } - end := offset + length - if end > len(c.items) { - end = len(c.items) - } - removed := make([]T, end-offset) - copy(removed, c.items[offset:end]) - c.items = append(c.items[:offset], c.items[end:]...) - return Collect(removed) -} - -// SpliceReplace removes a portion at the given offset and replaces it with the provided items. -// It returns the removed items. -func (c *Collection[T]) SpliceReplace(offset, length int, replacement []T) *Collection[T] { - if offset < 0 { - offset = len(c.items) + offset - if offset < 0 { - offset = 0 - } - } - end := offset + length - if end > len(c.items) { - end = len(c.items) - } - removed := make([]T, end-offset) - copy(removed, c.items[offset:end]) - newItems := make([]T, 0, len(c.items)-length+len(replacement)) - newItems = append(newItems, c.items[:offset]...) - newItems = append(newItems, replacement...) - newItems = append(newItems, c.items[end:]...) - c.items = newItems - return Collect(removed) -} - -// Take returns a new collection with the specified number of items from the front. -// A negative limit takes from the end. -func (c *Collection[T]) Take(limit int) *Collection[T] { - if limit < 0 { - return c.Slice(limit) - } - return c.Slice(0, limit) -} - -// TakeUntil returns items from the start until the callback returns true. -func (c *Collection[T]) TakeUntil(callback func(T, int) bool) *Collection[T] { - result := make([]T, 0) - for i, item := range c.items { - if callback(item, i) { - break - } - result = append(result, item) - } - return Collect(result) -} - -// TakeWhile returns items from the start as long as the callback returns true. -func (c *Collection[T]) TakeWhile(callback func(T, int) bool) *Collection[T] { - return c.TakeUntil(func(item T, index int) bool { - return !callback(item, index) - }) -} - -// Skip returns a new collection with the first n items removed. -func (c *Collection[T]) Skip(count int) *Collection[T] { - return c.Slice(count) -} - -// SkipUntil skips items until the callback returns true, then returns the rest. -func (c *Collection[T]) SkipUntil(callback func(T, int) bool) *Collection[T] { - result := make([]T, 0) - found := false - for i, item := range c.items { - if !found && callback(item, i) { - found = true - } - if found { - result = append(result, item) - } - } - return Collect(result) -} - -// SkipWhile skips items as long as the callback returns true, then returns the rest. -func (c *Collection[T]) SkipWhile(callback func(T, int) bool) *Collection[T] { - return c.SkipUntil(func(item T, index int) bool { - return !callback(item, index) - }) -} - -// Nth returns a new collection containing every n-th element, starting at an optional offset. -func (c *Collection[T]) Nth(step int, offsets ...int) *Collection[T] { - offset := 0 - if len(offsets) > 0 { - offset = offsets[0] - } - result := make([]T, 0) - for i := offset; i < len(c.items); i += step { - result = append(result, c.items[i]) - } - return Collect(result) -} - -// ForPage returns a subset of items for the given page number and page size. -func (c *Collection[T]) ForPage(page, perPage int) *Collection[T] { - offset := (page - 1) * perPage - return c.Slice(offset, perPage) -} - -// Values returns a new collection with re-indexed items (a shallow copy). -func (c *Collection[T]) Values() *Collection[T] { - result := make([]T, len(c.items)) - copy(result, c.items) - return Collect(result) -} - -// Keys returns a new Collection[int] containing the indices 0 through n-1. -func (c *Collection[T]) Keys() *Collection[int] { - keys := make([]int, len(c.items)) - for i := range c.items { - keys[i] = i - } - return Collect(keys) -} - -// Reverse returns a new collection with items in reverse order. -func (c *Collection[T]) Reverse() *Collection[T] { - result := make([]T, len(c.items)) - for i, j := 0, len(c.items)-1; j >= 0; i, j = i+1, j-1 { - result[i] = c.items[j] - } - return Collect(result) -} - -// Shuffle returns a new collection with items in random order. -func (c *Collection[T]) Shuffle() *Collection[T] { - result := make([]T, len(c.items)) - copy(result, c.items) - rand.Shuffle(len(result), func(i, j int) { - result[i], result[j] = result[j], result[i] - }) - return Collect(result) -} - -// Random returns a new collection with the specified number of randomly selected items. -func (c *Collection[T]) Random(counts ...int) *Collection[T] { - count := 1 - if len(counts) > 0 { - count = counts[0] - } - shuffled := c.Shuffle() - if count >= len(shuffled.items) { - return shuffled - } - return Collect(shuffled.items[:count]) -} - -// Sort returns a new collection sorted using the provided comparison function. -func (c *Collection[T]) Sort(less func(a, b T) bool) *Collection[T] { - result := make([]T, len(c.items)) - copy(result, c.items) - sort.SliceStable(result, func(i, j int) bool { - return less(result[i], result[j]) - }) - return Collect(result) -} - -// SortBy returns a new collection sorted in ascending order by the given key function. -func SortBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) *Collection[T] { - result := make([]T, len(c.items)) - copy(result, c.items) - sort.SliceStable(result, func(i, j int) bool { - return keyFunc(result[i]) < keyFunc(result[j]) - }) - return Collect(result) -} - -// SortByDesc returns a new collection sorted in descending order by the given key function. -func SortByDesc[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) *Collection[T] { - result := make([]T, len(c.items)) - copy(result, c.items) - sort.SliceStable(result, func(i, j int) bool { - return keyFunc(result[i]) > keyFunc(result[j]) - }) - return Collect(result) -} - -// SortDesc returns a new collection sorted in descending order using the provided comparison function. -func (c *Collection[T]) SortDesc(less func(a, b T) bool) *Collection[T] { - return c.Sort(func(a, b T) bool { - return less(b, a) - }) -} - -// Unique returns a new collection containing only items with distinct keys -// as determined by the given key function. -func Unique[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] { - seen := make(map[K]bool) - result := make([]T, 0) - for _, item := range c.items { - key := keyFunc(item) - if !seen[key] { - seen[key] = true - result = append(result, item) - } - } - return Collect(result) -} - -// Duplicates returns a new collection containing all duplicate items -// as determined by the given key function. -func Duplicates[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] { - seen := make(map[K]bool) - result := make([]T, 0) - for _, item := range c.items { - key := keyFunc(item) - if seen[key] { - result = append(result, item) - } else { - seen[key] = true - } - } - return Collect(result) -} - -// Every reports whether all items in the collection satisfy the given predicate. -func (c *Collection[T]) Every(callback func(T, int) bool) bool { - for i, item := range c.items { - if !callback(item, i) { - return false - } - } - return true -} - -// Partition splits the collection into two: items that pass the predicate and items that do not. -func (c *Collection[T]) Partition(callback func(T, int) bool) (*Collection[T], *Collection[T]) { - pass := make([]T, 0) - fail := make([]T, 0) - for i, item := range c.items { - if callback(item, i) { - pass = append(pass, item) - } else { - fail = append(fail, item) - } - } - return Collect(pass), Collect(fail) -} - -// Concat returns a new collection with the given items appended. -func (c *Collection[T]) Concat(items []T) *Collection[T] { - result := make([]T, len(c.items)+len(items)) - copy(result, c.items) - copy(result[len(c.items):], items) - return Collect(result) -} - -// Merge returns a new collection with the given items merged in (appended). -func (c *Collection[T]) Merge(items []T) *Collection[T] { - return c.Concat(items) -} - -// Pad pads the collection to the specified length with the given value. -// A negative size pads on the left; a positive size pads on the right. -func (c *Collection[T]) Pad(size int, value T) *Collection[T] { - absSize := size - if absSize < 0 { - absSize = -absSize - } - if len(c.items) >= absSize { - result := make([]T, len(c.items)) - copy(result, c.items) - return Collect(result) - } - padCount := absSize - len(c.items) - padding := make([]T, padCount) - for i := range padding { - padding[i] = value - } - if size < 0 { - result := make([]T, absSize) - copy(result, padding) - copy(result[padCount:], c.items) - return Collect(result) - } - result := make([]T, absSize) - copy(result, c.items) - copy(result[len(c.items):], padding) - return Collect(result) -} - -// Multiply returns a new collection with all items repeated the given number of times. -func (c *Collection[T]) Multiply(multiplier int) *Collection[T] { - if multiplier <= 0 { - return Empty[T]() - } - result := make([]T, 0, len(c.items)*multiplier) - for i := 0; i < multiplier; i++ { - result = append(result, c.items...) - } - return Collect(result) -} - -// Flip returns a new collection with the item order reversed. -// For typed Go slices this reverses the element order. -func (c *Collection[T]) Flip() *Collection[T] { - result := make([]T, len(c.items)) - for i, j := 0, len(c.items)-1; j >= 0; i, j = i+1, j-1 { - result[i] = c.items[j] - } - return Collect(result) -} - -// Forget removes an item from the collection by index, mutating the collection. -func (c *Collection[T]) Forget(index int) *Collection[T] { - if index < 0 || index >= len(c.items) { - return c - } - c.items = append(c.items[:index], c.items[index+1:]...) - return c -} - -// Implode joins elements into a string using the given glue, converting each item via fmt.Sprint. -func (c *Collection[T]) Implode(glue string) string { - parts := make([]string, len(c.items)) - for i, item := range c.items { - parts[i] = fmt.Sprint(item) - } - return strings.Join(parts, glue) -} - -// Join joins elements into a string using the given glue, -// with an optional final separator between the last two items. -func (c *Collection[T]) Join(glue string, finalGlues ...string) string { - if len(c.items) == 0 { - return "" - } - if len(c.items) == 1 { - return fmt.Sprint(c.items[0]) - } - parts := make([]string, len(c.items)) - for i, item := range c.items { - parts[i] = fmt.Sprint(item) - } - if len(finalGlues) > 0 && finalGlues[0] != "" { - finalGlue := finalGlues[0] - last := parts[len(parts)-1] - parts = parts[:len(parts)-1] - return strings.Join(parts, glue) + finalGlue + last - } - return strings.Join(parts, glue) -} - -// When applies the callback if the condition is true; otherwise it applies the optional default callback. -func (c *Collection[T]) When(condition bool, callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { - if condition { - return callback(c) - } - if len(defaults) > 0 { - return defaults[0](c) - } - return c -} - -// WhenEmpty applies the callback when the collection is empty. -func (c *Collection[T]) WhenEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { - return c.When(c.IsEmpty(), callback, defaults...) -} - -// WhenNotEmpty applies the callback when the collection is not empty. -func (c *Collection[T]) WhenNotEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { - return c.When(c.IsNotEmpty(), callback, defaults...) -} - -// Unless applies the callback unless the condition is true. -func (c *Collection[T]) Unless(condition bool, callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { - return c.When(!condition, callback, defaults...) -} - -// UnlessEmpty applies the callback unless the collection is empty. -func (c *Collection[T]) UnlessEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { - return c.WhenNotEmpty(callback, defaults...) -} - -// UnlessNotEmpty applies the callback unless the collection is not empty. -func (c *Collection[T]) UnlessNotEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { - return c.WhenEmpty(callback, defaults...) -} - -// Zip merges the collection with each of the given slices element-by-element. -func Zip[T any](c *Collection[T], others ...[]T) *Collection[[]T] { - maxLen := len(c.items) - for _, o := range others { - if len(o) > maxLen { - maxLen = len(o) - } - } - result := make([][]T, maxLen) - for i := 0; i < maxLen; i++ { - group := make([]T, 0, 1+len(others)) - if i < len(c.items) { - group = append(group, c.items[i]) - } else { - var zero T - group = append(group, zero) - } - for _, o := range others { - if i < len(o) { - group = append(group, o[i]) - } else { - var zero T - group = append(group, zero) - } - } - result[i] = group - } - return Collect(result) -} - -// CrossJoin returns the cross product of the collection with the given slices. -func CrossJoin[T any](c *Collection[T], others ...[]T) *Collection[[]T] { - results := [][]T{{}} - allLists := append([][]T{c.items}, others...) - for _, list := range allLists { - var newResults [][]T - for _, result := range results { - for _, item := range list { - newResult := make([]T, len(result)+1) - copy(newResult, result) - newResult[len(result)] = item - newResults = append(newResults, newResult) - } - } - results = newResults - } - return Collect(results) -} - -// Combine pairs keys from this collection with values from the given slice, -// returning a collection of Pair values. -func Combine[K any, V any](keys *Collection[K], values []V) *Collection[Pair[K, V]] { - minLen := len(keys.items) - if len(values) < minLen { - minLen = len(values) - } - result := make([]Pair[K, V], minLen) - for i := 0; i < minLen; i++ { - result[i] = Pair[K, V]{Key: keys.items[i], Value: values[i]} - } - return Collect(result) -} - -// Collapse flattens a collection of slices into a single, flat collection. -func Collapse[T any](c *Collection[[]T]) *Collection[T] { - result := make([]T, 0) - for _, items := range c.items { - result = append(result, items...) - } - return Collect(result) -} - -// Diff returns the items in the collection that are not present in the given slice. -func Diff[T comparable](c *Collection[T], items []T) *Collection[T] { - lookup := make(map[T]bool, len(items)) - for _, item := range items { - lookup[item] = true - } - result := make([]T, 0) - for _, item := range c.items { - if !lookup[item] { - result = append(result, item) - } - } - return Collect(result) -} - -// DiffUsing returns items not present in the given slice, using a custom equality function. -func (c *Collection[T]) DiffUsing(items []T, equals func(T, T) bool) *Collection[T] { - result := make([]T, 0) - for _, item := range c.items { - found := false - for _, other := range items { - if equals(item, other) { - found = true - break - } - } - if !found { - result = append(result, item) - } - } - return Collect(result) -} - -// Intersect returns the items present in both the collection and the given slice. -func Intersect[T comparable](c *Collection[T], items []T) *Collection[T] { - lookup := make(map[T]bool, len(items)) - for _, item := range items { - lookup[item] = true - } - result := make([]T, 0) - for _, item := range c.items { - if lookup[item] { - result = append(result, item) - } - } - return Collect(result) -} - -// IntersectUsing returns items present in both the collection and the given slice, -// using a custom equality function. -func (c *Collection[T]) IntersectUsing(items []T, equals func(T, T) bool) *Collection[T] { - result := make([]T, 0) - for _, item := range c.items { - for _, other := range items { - if equals(item, other) { - result = append(result, item) - break - } - } - } - return Collect(result) -} - -// ToSlice returns a copy of the underlying slice. -func (c *Collection[T]) ToSlice() []T { - result := make([]T, len(c.items)) - copy(result, c.items) - return result -} - -// ToJSON serializes the collection to JSON bytes. -func (c *Collection[T]) ToJSON() ([]byte, error) { - return json.Marshal(c.items) -} - -// ToPrettyJSON serializes the collection to indented JSON bytes. -func (c *Collection[T]) ToPrettyJSON() ([]byte, error) { - return json.MarshalIndent(c.items, "", " ") -} - -// String returns the JSON string representation of the collection. -func (c *Collection[T]) String() string { - b, err := c.ToJSON() - if err != nil { - return "[]" - } - return string(b) -} - -// MarshalJSON implements the json.Marshaler interface. -func (c *Collection[T]) MarshalJSON() ([]byte, error) { - return json.Marshal(c.items) -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (c *Collection[T]) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &c.items) -} - -// Copy creates a shallow copy of the collection. -func (c *Collection[T]) Copy() *Collection[T] { - result := make([]T, len(c.items)) - copy(result, c.items) - return Collect(result) -} - // Len returns the number of items, implementing sort.Interface. func (c *Collection[T]) Len() int { return len(c.items) } -// TapEach calls the given callback on each item for side effects, returning the original collection. -func (c *Collection[T]) TapEach(callback func(T, int)) *Collection[T] { - for i, item := range c.items { - callback(item, i) - } - return c -} - -// Dump prints the collection items to stdout for debugging. -func (c *Collection[T]) Dump() *Collection[T] { - fmt.Printf("%v\n", c.items) - return c -} - -// DD prints the collection items for debugging and terminates the program. -func (c *Collection[T]) DD() { - c.Dump() - os.Exit(1) -} - -// Only returns a new collection containing only items at the given indices. -func (c *Collection[T]) Only(indices ...int) *Collection[T] { - result := make([]T, 0, len(indices)) - for _, idx := range indices { - if idx >= 0 && idx < len(c.items) { - result = append(result, c.items[idx]) - } - } - return Collect(result) -} - -// Except returns a new collection excluding items at the given indices. -func (c *Collection[T]) Except(indices ...int) *Collection[T] { - excludeSet := make(map[int]bool, len(indices)) - for _, idx := range indices { - excludeSet[idx] = true - } - result := make([]T, 0) - for i, item := range c.items { - if !excludeSet[i] { - result = append(result, item) - } - } - return Collect(result) -} - -// Has reports whether the given index exists in the collection. -// Negative indices count from the end. -func (c *Collection[T]) Has(index int) bool { - if index < 0 { - index = len(c.items) + index - } - return index >= 0 && index < len(c.items) -} - -// HasAny reports whether any of the given indices exist in the collection. -func (c *Collection[T]) HasAny(indices ...int) bool { - for _, idx := range indices { - if c.Has(idx) { - return true - } - } - return false -} - -// Pluck extracts a value from each item using the given function, returning a new collection. -func Pluck[T any, V any](c *Collection[T], valueFunc func(T) V) *Collection[V] { - result := make([]V, len(c.items)) - for i, item := range c.items { - result[i] = valueFunc(item) - } - return Collect(result) -} - -// GroupBy groups the collection's items by a key returned from the given function. -func GroupBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]*Collection[T] { - groups := make(map[K]*Collection[T]) - for _, item := range c.items { - key := keyFunc(item) - if _, ok := groups[key]; !ok { - groups[key] = Empty[T]() - } - groups[key].Push(item) - } - return groups -} - -// KeyBy indexes the collection by a key returned from the given function. -func KeyBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]T { - result := make(map[K]T) - for _, item := range c.items { - result[keyFunc(item)] = item - } - return result -} - -// CountBy counts how many items produce each key from the given function. -func CountBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]int { - result := make(map[K]int) - for _, item := range c.items { - result[keyFunc(item)]++ - } - return result -} - -// MapToDictionary maps each item to a key-value pair and groups values by key. -func MapToDictionary[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K][]V { - result := make(map[K][]V) - for _, item := range c.items { - key, value := callback(item) - result[key] = append(result[key], value) - } - return result -} - -// MapToGroups is an alias for MapToDictionary. -func MapToGroups[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K][]V { - return MapToDictionary(c, callback) -} - -// MapWithKeys maps each item to a key-value pair, returning a map. -func MapWithKeys[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K]V { - result := make(map[K]V) - for _, item := range c.items { - key, value := callback(item) - result[key] = value - } - return result -} - -// Where filters items using a predicate that receives only the item (no index). -func (c *Collection[T]) Where(predicate func(T) bool) *Collection[T] { - return c.Filter(func(item T, _ int) bool { - return predicate(item) - }) -} - -// WhereNot filters items using a negative predicate that receives only the item (no index). -func (c *Collection[T]) WhereNot(predicate func(T) bool) *Collection[T] { - return c.Filter(func(item T, _ int) bool { - return !predicate(item) - }) -} - -// WhereNull returns items whose extracted value equals the zero value. -func WhereNull[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] { - var zero K - result := make([]T, 0) - for _, item := range c.items { - if keyFunc(item) == zero { - result = append(result, item) - } - } - return Collect(result) -} - -// WhereNotNull returns items whose extracted value is not the zero value. -func WhereNotNull[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] { - var zero K - result := make([]T, 0) - for _, item := range c.items { - if keyFunc(item) != zero { - result = append(result, item) - } - } - return Collect(result) -} - -// WhereIn returns items whose extracted key value is in the given set. -func WhereIn[T any, K comparable](c *Collection[T], keyFunc func(T) K, values []K) *Collection[T] { - set := make(map[K]bool, len(values)) - for _, v := range values { - set[v] = true - } - result := make([]T, 0) - for _, item := range c.items { - if set[keyFunc(item)] { - result = append(result, item) - } - } - return Collect(result) -} - -// WhereNotIn returns items whose extracted key value is not in the given set. -func WhereNotIn[T any, K comparable](c *Collection[T], keyFunc func(T) K, values []K) *Collection[T] { - set := make(map[K]bool, len(values)) - for _, v := range values { - set[v] = true - } - result := make([]T, 0) - for _, item := range c.items { - if !set[keyFunc(item)] { - result = append(result, item) - } - } - return Collect(result) -} - -// WhereBetween returns items whose extracted key value is between min and max (inclusive). -func WhereBetween[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K, min, max K) *Collection[T] { - result := make([]T, 0) - for _, item := range c.items { - v := keyFunc(item) - if v >= min && v <= max { - result = append(result, item) - } - } - return Collect(result) -} - -// WhereNotBetween returns items whose extracted key value is outside the range [min, max]. -func WhereNotBetween[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K, min, max K) *Collection[T] { - result := make([]T, 0) - for _, item := range c.items { - v := keyFunc(item) - if v < min || v > max { - result = append(result, item) - } - } - return Collect(result) -} - -// Dot returns a shallow copy of the collection. -// For typed Go slices, no dot-notation expansion is possible. -func (c *Collection[T]) Dot() *Collection[T] { - return c.Copy() -} - -// Undot returns a shallow copy of the collection. -// For typed Go slices, no dot-notation expansion is possible. -func (c *Collection[T]) Undot() *Collection[T] { - return c.Copy() -} - -// Ensure verifies that all items satisfy the given predicate, returning an error if any do not. -func (c *Collection[T]) Ensure(predicate func(T) bool) error { - for _, item := range c.items { - if !predicate(item) { - return fmt.Errorf("collection item failed ensure check") - } - } - return nil -} - // ToBase returns the collection itself. func (c *Collection[T]) ToBase() *Collection[T] { return c } -// Lazy returns a new LazyCollection backed by the collection's items. -func (c *Collection[T]) Lazy() *LazyCollection[T] { - items := c.items - return NewLazy(func(yield func(T) bool) { - for _, item := range items { - if !yield(item) { - return - } - } - }) -} - -// Median returns the median value of a float64 collection. -func Median(c *Collection[float64]) float64 { - if len(c.items) == 0 { - return 0 - } - sorted := make([]float64, len(c.items)) - copy(sorted, c.items) - slices.Sort(sorted) - mid := len(sorted) / 2 - if len(sorted)%2 == 0 { - return (sorted[mid-1] + sorted[mid]) / 2 - } - return sorted[mid] -} - -// MedianBy returns the median value extracted from each item by the given function. -func MedianBy[T any](c *Collection[T], valueFunc func(T) float64) float64 { - return Median(Map(c, func(item T, _ int) float64 { - return valueFunc(item) - })) -} - -// Mode returns the most frequently occurring values in the collection. -func Mode[T comparable](c *Collection[T]) []T { - if len(c.items) == 0 { - return nil - } - counts := make(map[T]int) - maxCount := 0 - for _, item := range c.items { - counts[item]++ - if counts[item] > maxCount { - maxCount = counts[item] - } - } - result := make([]T, 0) - for item, count := range counts { - if count == maxCount { - result = append(result, item) - } - } - return result -} - -// Sum returns the sum of all items in a numeric collection. -func Sum[T Numeric](c *Collection[T]) T { - var total T - for _, item := range c.items { - total += item - } - return total -} - -// SumBy returns the sum of values extracted from each item by the given function. -func SumBy[T any, N Numeric](c *Collection[T], valueFunc func(T) N) N { - var total N - for _, item := range c.items { - total += valueFunc(item) - } - return total -} - -// Avg returns the arithmetic mean of all items in a numeric collection. -func Avg[T Numeric](c *Collection[T]) float64 { - if len(c.items) == 0 { - return 0 - } - return float64(Sum(c)) / float64(len(c.items)) -} - -// AvgBy returns the arithmetic mean of values extracted from each item by the given function. -func AvgBy[T any, N Numeric](c *Collection[T], valueFunc func(T) N) float64 { - if len(c.items) == 0 { - return 0 - } - return float64(SumBy(c, valueFunc)) / float64(len(c.items)) -} - -// Average is an alias for Avg. -func Average[T Numeric](c *Collection[T]) float64 { - return Avg(c) -} - -// Min returns the minimum value in an ordered collection. -// The second return value indicates whether the collection was non-empty. -func Min[T cmp.Ordered](c *Collection[T]) (T, bool) { - if len(c.items) == 0 { - var zero T - return zero, false - } - result := c.items[0] - for _, item := range c.items[1:] { - if item < result { - result = item - } - } - return result, true -} - -// MinBy returns the item with the minimum key as determined by the given function. -// The second return value indicates whether the collection was non-empty. -func MinBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) (T, bool) { - if len(c.items) == 0 { - var zero T - return zero, false - } - result := c.items[0] - minKey := keyFunc(result) - for _, item := range c.items[1:] { - k := keyFunc(item) - if k < minKey { - minKey = k - result = item - } - } - return result, true -} - -// Max returns the maximum value in an ordered collection. -// The second return value indicates whether the collection was non-empty. -func Max[T cmp.Ordered](c *Collection[T]) (T, bool) { - if len(c.items) == 0 { - var zero T - return zero, false - } - result := c.items[0] - for _, item := range c.items[1:] { - if item > result { - result = item - } - } - return result, true -} - -// MaxBy returns the item with the maximum key as determined by the given function. -// The second return value indicates whether the collection was non-empty. -func MaxBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) (T, bool) { - if len(c.items) == 0 { - var zero T - return zero, false - } - result := c.items[0] - maxKey := keyFunc(result) - for _, item := range c.items[1:] { - k := keyFunc(item) - if k > maxKey { - maxKey = k - result = item - } - } - return result, true -} - // Iter returns an iter.Seq[T] that yields each item in the collection. func (c *Collection[T]) Iter() iter.Seq[T] { return func(yield func(T) bool) { @@ -1631,8 +152,8 @@ func (c *Collection[T]) Iter() iter.Seq[T] { } } -// Iter2 returns an iter.Seq2[int, T] that yields each index-item pair in the collection. -func (c *Collection[T]) Iter2() iter.Seq2[int, T] { +// PairIter returns an iter.Seq2[int, T] that yields each index-item pair in the collection. +func (c *Collection[T]) PairIter() iter.Seq2[int, T] { return func(yield func(int, T) bool) { for i, item := range c.items { if !yield(i, item) { diff --git a/collection/collection_test.go b/collection/collection_test.go index 2bceaac..114b39f 100644 --- a/collection/collection_test.go +++ b/collection/collection_test.go @@ -4,15 +4,19 @@ import ( "encoding/json" "reflect" "sort" + "strings" "testing" ) func TestNew(t *testing.T) { c := New(1, 2, 3) + if c.Count() != 3 { t.Errorf("expected 3 items, got %d", c.Count()) } + expected := []int{1, 2, 3} + if !reflect.DeepEqual(c.All(), expected) { t.Errorf("expected %v, got %v", expected, c.All()) } @@ -21,6 +25,7 @@ func TestNew(t *testing.T) { func TestCollect(t *testing.T) { items := []string{"a", "b", "c"} c := Collect(items) + if c.Count() != 3 { t.Errorf("expected 3, got %d", c.Count()) } @@ -28,9 +33,11 @@ func TestCollect(t *testing.T) { func TestEmpty(t *testing.T) { c := Empty[int]() + if !c.IsEmpty() { t.Error("expected empty collection") } + if c.IsNotEmpty() { t.Error("expected empty collection") } @@ -39,6 +46,7 @@ func TestEmpty(t *testing.T) { func TestTimes(t *testing.T) { c := Times(5, func(i int) int { return i * 2 }) expected := []int{2, 4, 6, 8, 10} + if !reflect.DeepEqual(c.All(), expected) { t.Errorf("expected %v, got %v", expected, c.All()) } @@ -47,12 +55,14 @@ func TestTimes(t *testing.T) { func TestRange(t *testing.T) { c := Range(1, 5) expected := []int{1, 2, 3, 4, 5} + if !reflect.DeepEqual(c.All(), expected) { t.Errorf("expected %v, got %v", expected, c.All()) } c2 := Range(5, 1) expected2 := []int{5, 4, 3, 2, 1} + if !reflect.DeepEqual(c2.All(), expected2) { t.Errorf("expected %v, got %v", expected2, c2.All()) } @@ -62,6 +72,7 @@ func TestContainsOneItem(t *testing.T) { if !New(1).ContainsOneItem() { t.Error("expected true") } + if New(1, 2).ContainsOneItem() { t.Error("expected false") } @@ -71,6 +82,7 @@ func TestContainsManyItems(t *testing.T) { if !New(1, 2).ContainsManyItems() { t.Error("expected true") } + if New(1).ContainsManyItems() { t.Error("expected false") } @@ -80,16 +92,19 @@ func TestFirst(t *testing.T) { c := New(1, 2, 3, 4, 5) v, ok := c.First() + if !ok || v != 1 { t.Errorf("expected 1, got %d", v) } v, ok = c.First(func(item int, _ int) bool { return item > 3 }) + if !ok || v != 4 { t.Errorf("expected 4, got %d", v) } _, ok = c.First(func(item int, _ int) bool { return item > 10 }) + if ok { t.Error("expected not found") } @@ -98,12 +113,14 @@ func TestFirst(t *testing.T) { func TestFirstOrFail(t *testing.T) { c := New(1, 2, 3) v, err := c.FirstOrFail() + if err != nil || v != 1 { t.Errorf("expected 1, got %d, err: %v", v, err) } empty := Empty[int]() _, err = empty.FirstOrFail() + if err == nil { t.Error("expected error") } @@ -113,11 +130,13 @@ func TestLast(t *testing.T) { c := New(1, 2, 3, 4, 5) v, ok := c.Last() + if !ok || v != 5 { t.Errorf("expected 5, got %d", v) } v, ok = c.Last(func(item int, _ int) bool { return item < 3 }) + if !ok || v != 2 { t.Errorf("expected 2, got %d", v) } @@ -126,16 +145,19 @@ func TestLast(t *testing.T) { func TestSole(t *testing.T) { c := New(1, 2, 3) v, err := c.Sole(func(item int, _ int) bool { return item == 2 }) + if err != nil || v != 2 { t.Errorf("expected 2, got %d, err: %v", v, err) } _, err = c.Sole(func(item int, _ int) bool { return item > 1 }) + if err == nil { t.Error("expected MultipleItemsFoundError") } _, err = c.Sole(func(item int, _ int) bool { return item > 10 }) + if err == nil { t.Error("expected ItemNotFoundError") } @@ -145,21 +167,25 @@ func TestGet(t *testing.T) { c := New(10, 20, 30) v, ok := c.Get(1) + if !ok || v != 20 { t.Errorf("expected 20, got %d", v) } v, ok = c.Get(-1) + if !ok || v != 30 { t.Errorf("expected 30 for negative index, got %d", v) } _, ok = c.Get(10) + if ok { t.Error("expected not found") } v, _ = c.Get(10, 99) + if v != 99 { t.Errorf("expected default 99, got %d", v) } @@ -168,14 +194,17 @@ func TestGet(t *testing.T) { func TestGetOrPut(t *testing.T) { c := New(1, 2, 3) v := c.GetOrPut(1, 99) + if v != 2 { t.Errorf("expected existing value 2, got %d", v) } v = c.GetOrPut(10, 99) + if v != 99 { t.Errorf("expected default 99, got %d", v) } + if c.Count() != 4 { t.Errorf("expected 4 items after put, got %d", c.Count()) } @@ -185,6 +214,7 @@ func TestPut(t *testing.T) { c := New(1, 2, 3) c.Put(1, 99) v, _ := c.Get(1) + if v != 99 { t.Errorf("expected 99, got %d", v) } @@ -193,9 +223,11 @@ func TestPut(t *testing.T) { func TestPull(t *testing.T) { c := New(10, 20, 30) v, ok := c.Pull(1) + if !ok || v != 20 { t.Errorf("expected 20, got %d", v) } + if c.Count() != 2 { t.Errorf("expected 2 items, got %d", c.Count()) } @@ -203,9 +235,11 @@ func TestPull(t *testing.T) { func TestContains(t *testing.T) { c := New(1, 2, 3, 4, 5) + if !c.Contains(func(item int, _ int) bool { return item == 3 }) { t.Error("expected to contain 3") } + if c.Contains(func(item int, _ int) bool { return item == 10 }) { t.Error("expected not to contain 10") } @@ -213,6 +247,7 @@ func TestContains(t *testing.T) { func TestDoesntContain(t *testing.T) { c := New(1, 2, 3) + if !c.DoesntContain(func(item int, _ int) bool { return item == 10 }) { t.Error("expected not to contain 10") } @@ -221,11 +256,13 @@ func TestDoesntContain(t *testing.T) { func TestSearch(t *testing.T) { c := New(10, 20, 30) idx, ok := c.Search(func(item int, _ int) bool { return item == 20 }) + if !ok || idx != 1 { t.Errorf("expected index 1, got %d", idx) } _, ok = c.Search(func(item int, _ int) bool { return item == 99 }) + if ok { t.Error("expected not found") } @@ -234,11 +271,13 @@ func TestSearch(t *testing.T) { func TestBefore(t *testing.T) { c := New(1, 2, 3, 4, 5) v, ok := c.Before(func(item int, _ int) bool { return item == 3 }) + if !ok || v != 2 { t.Errorf("expected 2, got %d", v) } _, ok = c.Before(func(item int, _ int) bool { return item == 1 }) + if ok { t.Error("expected not found for first element") } @@ -247,11 +286,13 @@ func TestBefore(t *testing.T) { func TestAfter(t *testing.T) { c := New(1, 2, 3, 4, 5) v, ok := c.After(func(item int, _ int) bool { return item == 3 }) + if !ok || v != 4 { t.Errorf("expected 4, got %d", v) } _, ok = c.After(func(item int, _ int) bool { return item == 5 }) + if ok { t.Error("expected not found for last element") } @@ -260,6 +301,7 @@ func TestAfter(t *testing.T) { func TestPush(t *testing.T) { c := New(1, 2) c.Push(3, 4) + if c.Count() != 4 { t.Errorf("expected 4, got %d", c.Count()) } @@ -269,6 +311,7 @@ func TestPrepend(t *testing.T) { c := New(2, 3) c.Prepend(1) expected := []int{1, 2, 3} + if !reflect.DeepEqual(c.All(), expected) { t.Errorf("expected %v, got %v", expected, c.All()) } @@ -277,9 +320,11 @@ func TestPrepend(t *testing.T) { func TestPop(t *testing.T) { c := New(1, 2, 3) v, ok := c.Pop() + if !ok || v != 3 { t.Errorf("expected 3, got %d", v) } + if c.Count() != 2 { t.Errorf("expected 2 items, got %d", c.Count()) } @@ -288,9 +333,11 @@ func TestPop(t *testing.T) { func TestPopMany(t *testing.T) { c := New(1, 2, 3, 4, 5) popped := c.PopMany(2) + if !reflect.DeepEqual(popped.All(), []int{4, 5}) { t.Errorf("expected [4 5], got %v", popped.All()) } + if c.Count() != 3 { t.Errorf("expected 3 items, got %d", c.Count()) } @@ -299,9 +346,11 @@ func TestPopMany(t *testing.T) { func TestShift(t *testing.T) { c := New(1, 2, 3) v, ok := c.Shift() + if !ok || v != 1 { t.Errorf("expected 1, got %d", v) } + if c.Count() != 2 { t.Errorf("expected 2, got %d", c.Count()) } @@ -310,9 +359,11 @@ func TestShift(t *testing.T) { func TestShiftMany(t *testing.T) { c := New(1, 2, 3, 4, 5) shifted := c.ShiftMany(2) + if !reflect.DeepEqual(shifted.All(), []int{1, 2}) { t.Errorf("expected [1 2], got %v", shifted.All()) } + if c.Count() != 3 { t.Errorf("expected 3 items, got %d", c.Count()) } @@ -323,8 +374,10 @@ func TestEach(t *testing.T) { sum := 0 c.Each(func(item int, _ int) bool { sum += item + return true }) + if sum != 15 { t.Errorf("expected sum 15, got %d", sum) } @@ -333,8 +386,10 @@ func TestEach(t *testing.T) { count := 0 c.Each(func(item int, _ int) bool { count++ + return item < 3 }) + if count != 3 { t.Errorf("expected 3 iterations, got %d", count) } @@ -344,6 +399,7 @@ func TestFilter(t *testing.T) { c := New(1, 2, 3, 4, 5) evens := c.Filter(func(item int, _ int) bool { return item%2 == 0 }) expected := []int{2, 4} + if !reflect.DeepEqual(evens.All(), expected) { t.Errorf("expected %v, got %v", expected, evens.All()) } @@ -353,6 +409,7 @@ func TestReject(t *testing.T) { c := New(1, 2, 3, 4, 5) odds := c.Reject(func(item int, _ int) bool { return item%2 == 0 }) expected := []int{1, 3, 5} + if !reflect.DeepEqual(odds.All(), expected) { t.Errorf("expected %v, got %v", expected, odds.All()) } @@ -362,6 +419,7 @@ func TestMap(t *testing.T) { c := New(1, 2, 3) doubled := Map(c, func(item int, _ int) int { return item * 2 }) expected := []int{2, 4, 6} + if !reflect.DeepEqual(doubled.All(), expected) { t.Errorf("expected %v, got %v", expected, doubled.All()) } @@ -373,6 +431,7 @@ func TestMapDifferentTypes(t *testing.T) { return string(rune('a' + item - 1)) }) expected := []string{"a", "b", "c"} + if !reflect.DeepEqual(strs.All(), expected) { t.Errorf("expected %v, got %v", expected, strs.All()) } @@ -382,6 +441,7 @@ func TestTransform(t *testing.T) { c := New(1, 2, 3) c.Transform(func(item int, _ int) int { return item * 10 }) expected := []int{10, 20, 30} + if !reflect.DeepEqual(c.All(), expected) { t.Errorf("expected %v, got %v", expected, c.All()) } @@ -391,6 +451,7 @@ func TestFlatMap(t *testing.T) { c := New(1, 2, 3) result := FlatMap(c, func(item int, _ int) []int { return []int{item, item * 10} }) expected := []int{1, 10, 2, 20, 3, 30} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -399,6 +460,7 @@ func TestFlatMap(t *testing.T) { func TestReduce(t *testing.T) { c := New(1, 2, 3, 4, 5) sum := Reduce(c, func(carry int, item int, _ int) int { return carry + item }, 0) + if sum != 15 { t.Errorf("expected 15, got %d", sum) } @@ -407,12 +469,15 @@ func TestReduce(t *testing.T) { func TestChunk(t *testing.T) { c := New(1, 2, 3, 4, 5) chunks := c.Chunk(2) + if len(chunks) != 3 { t.Errorf("expected 3 chunks, got %d", len(chunks)) } + if !reflect.DeepEqual(chunks[0], []int{1, 2}) { t.Errorf("expected [1 2], got %v", chunks[0]) } + if !reflect.DeepEqual(chunks[2], []int{5}) { t.Errorf("expected [5], got %v", chunks[2]) } @@ -423,6 +488,7 @@ func TestChunkWhile(t *testing.T) { chunks := c.ChunkWhile(func(item int, _ int, current []int) bool { return item == current[len(current)-1] }) + if len(chunks) != 3 { t.Errorf("expected 3 chunks, got %d", len(chunks)) } @@ -431,6 +497,7 @@ func TestChunkWhile(t *testing.T) { func TestSplit(t *testing.T) { c := New(1, 2, 3, 4, 5) groups := c.Split(3) + if len(groups) != 3 { t.Errorf("expected 3 groups, got %d", len(groups)) } @@ -439,15 +506,18 @@ func TestSplit(t *testing.T) { func TestSliding(t *testing.T) { c := New(1, 2, 3, 4, 5) windows := c.Sliding(3) + if len(windows) != 3 { t.Errorf("expected 3 windows, got %d", len(windows)) } + if !reflect.DeepEqual(windows[0], []int{1, 2, 3}) { t.Errorf("expected [1 2 3], got %v", windows[0]) } // With step windows2 := c.Sliding(2, 2) + if len(windows2) != 2 { t.Errorf("expected 2 windows, got %d", len(windows2)) } @@ -458,12 +528,14 @@ func TestSlice(t *testing.T) { s := c.Slice(2) expected := []int{3, 4, 5} + if !reflect.DeepEqual(s.All(), expected) { t.Errorf("expected %v, got %v", expected, s.All()) } s2 := c.Slice(1, 2) expected2 := []int{2, 3} + if !reflect.DeepEqual(s2.All(), expected2) { t.Errorf("expected %v, got %v", expected2, s2.All()) } @@ -471,6 +543,7 @@ func TestSlice(t *testing.T) { // Negative offset s3 := c.Slice(-2) expected3 := []int{4, 5} + if !reflect.DeepEqual(s3.All(), expected3) { t.Errorf("expected %v, got %v", expected3, s3.All()) } @@ -481,6 +554,7 @@ func TestTake(t *testing.T) { taken := c.Take(3) expected := []int{1, 2, 3} + if !reflect.DeepEqual(taken.All(), expected) { t.Errorf("expected %v, got %v", expected, taken.All()) } @@ -488,6 +562,7 @@ func TestTake(t *testing.T) { // Negative take taken2 := c.Take(-2) expected2 := []int{4, 5} + if !reflect.DeepEqual(taken2.All(), expected2) { t.Errorf("expected %v, got %v", expected2, taken2.All()) } @@ -497,6 +572,7 @@ func TestTakeUntil(t *testing.T) { c := New(1, 2, 3, 4, 5) result := c.TakeUntil(func(item int, _ int) bool { return item == 4 }) expected := []int{1, 2, 3} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -506,6 +582,7 @@ func TestTakeWhile(t *testing.T) { c := New(1, 2, 3, 4, 5) result := c.TakeWhile(func(item int, _ int) bool { return item < 4 }) expected := []int{1, 2, 3} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -515,6 +592,7 @@ func TestSkip(t *testing.T) { c := New(1, 2, 3, 4, 5) result := c.Skip(2) expected := []int{3, 4, 5} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -524,6 +602,7 @@ func TestSkipUntil(t *testing.T) { c := New(1, 2, 3, 4, 5) result := c.SkipUntil(func(item int, _ int) bool { return item == 3 }) expected := []int{3, 4, 5} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -533,6 +612,7 @@ func TestSkipWhile(t *testing.T) { c := New(1, 2, 3, 4, 5) result := c.SkipWhile(func(item int, _ int) bool { return item < 3 }) expected := []int{3, 4, 5} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -542,12 +622,14 @@ func TestNth(t *testing.T) { c := New(1, 2, 3, 4, 5, 6, 7, 8) result := c.Nth(3) expected := []int{1, 4, 7} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } result2 := c.Nth(3, 1) expected2 := []int{2, 5, 8} + if !reflect.DeepEqual(result2.All(), expected2) { t.Errorf("expected %v, got %v", expected2, result2.All()) } @@ -557,6 +639,7 @@ func TestForPage(t *testing.T) { c := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) result := c.ForPage(2, 3) expected := []int{4, 5, 6} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -566,6 +649,7 @@ func TestReverse(t *testing.T) { c := New(1, 2, 3, 4, 5) result := c.Reverse() expected := []int{5, 4, 3, 2, 1} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -574,6 +658,7 @@ func TestReverse(t *testing.T) { func TestShuffle(t *testing.T) { c := New(1, 2, 3, 4, 5) shuffled := c.Shuffle() + if shuffled.Count() != 5 { t.Errorf("expected 5 items, got %d", shuffled.Count()) } @@ -583,6 +668,7 @@ func TestSort(t *testing.T) { c := New(3, 1, 4, 1, 5, 9, 2, 6) sorted := c.Sort(func(a, b int) bool { return a < b }) expected := []int{1, 1, 2, 3, 4, 5, 6, 9} + if !reflect.DeepEqual(sorted.All(), expected) { t.Errorf("expected %v, got %v", expected, sorted.All()) } @@ -593,8 +679,10 @@ func TestSortBy(t *testing.T) { Name string Age int } + c := New(Item{"Bob", 30}, Item{"Alice", 25}, Item{"Charlie", 35}) sorted := SortBy(c, func(item Item) int { return item.Age }) + if sorted.All()[0].Name != "Alice" { t.Errorf("expected Alice first, got %s", sorted.All()[0].Name) } @@ -605,8 +693,10 @@ func TestSortByDesc(t *testing.T) { Name string Age int } + c := New(Item{"Bob", 30}, Item{"Alice", 25}, Item{"Charlie", 35}) sorted := SortByDesc(c, func(item Item) int { return item.Age }) + if sorted.All()[0].Name != "Charlie" { t.Errorf("expected Charlie first, got %s", sorted.All()[0].Name) } @@ -616,6 +706,7 @@ func TestUnique(t *testing.T) { c := New(1, 2, 2, 3, 3, 3, 4) unique := Unique(c, func(item int) int { return item }) expected := []int{1, 2, 3, 4} + if !reflect.DeepEqual(unique.All(), expected) { t.Errorf("expected %v, got %v", expected, unique.All()) } @@ -625,6 +716,7 @@ func TestDuplicates(t *testing.T) { c := New(1, 2, 2, 3, 3, 3) dups := Duplicates(c, func(item int) int { return item }) expected := []int{2, 3, 3} + if !reflect.DeepEqual(dups.All(), expected) { t.Errorf("expected %v, got %v", expected, dups.All()) } @@ -632,9 +724,11 @@ func TestDuplicates(t *testing.T) { func TestEvery(t *testing.T) { c := New(2, 4, 6, 8) + if !c.Every(func(item int, _ int) bool { return item%2 == 0 }) { t.Error("expected all even") } + if c.Every(func(item int, _ int) bool { return item > 5 }) { t.Error("expected not all > 5") } @@ -643,9 +737,11 @@ func TestEvery(t *testing.T) { func TestPartition(t *testing.T) { c := New(1, 2, 3, 4, 5) pass, fail := c.Partition(func(item int, _ int) bool { return item > 3 }) + if !reflect.DeepEqual(pass.All(), []int{4, 5}) { t.Errorf("expected [4 5], got %v", pass.All()) } + if !reflect.DeepEqual(fail.All(), []int{1, 2, 3}) { t.Errorf("expected [1 2 3], got %v", fail.All()) } @@ -655,6 +751,7 @@ func TestConcat(t *testing.T) { c := New(1, 2, 3) result := c.Concat([]int{4, 5}) expected := []int{1, 2, 3, 4, 5} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -665,12 +762,14 @@ func TestPad(t *testing.T) { padded := c.Pad(5, 0) expected := []int{1, 2, 3, 0, 0} + if !reflect.DeepEqual(padded.All(), expected) { t.Errorf("expected %v, got %v", expected, padded.All()) } paddedLeft := c.Pad(-5, 0) expected2 := []int{0, 0, 1, 2, 3} + if !reflect.DeepEqual(paddedLeft.All(), expected2) { t.Errorf("expected %v, got %v", expected2, paddedLeft.All()) } @@ -680,6 +779,7 @@ func TestMultiply(t *testing.T) { c := New(1, 2, 3) result := c.Multiply(3) expected := []int{1, 2, 3, 1, 2, 3, 1, 2, 3} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -689,6 +789,7 @@ func TestForget(t *testing.T) { c := New(1, 2, 3, 4, 5) c.Forget(2) expected := []int{1, 2, 4, 5} + if !reflect.DeepEqual(c.All(), expected) { t.Errorf("expected %v, got %v", expected, c.All()) } @@ -697,6 +798,7 @@ func TestForget(t *testing.T) { func TestImplode(t *testing.T) { c := New("a", "b", "c") result := c.Implode(", ") + if result != "a, b, c" { t.Errorf("expected 'a, b, c', got '%s'", result) } @@ -706,11 +808,13 @@ func TestJoin(t *testing.T) { c := New("a", "b", "c") result := c.Join(", ") + if result != "a, b, c" { t.Errorf("expected 'a, b, c', got '%s'", result) } result2 := c.Join(", ", " and ") + if result2 != "a, b and c" { t.Errorf("expected 'a, b and c', got '%s'", result2) } @@ -722,6 +826,7 @@ func TestWhen(t *testing.T) { result := c.When(true, func(c *Collection[int]) *Collection[int] { return c.Push(4) }) + if result.Count() != 4 { t.Errorf("expected 4 items, got %d", result.Count()) } @@ -730,6 +835,7 @@ func TestWhen(t *testing.T) { result2 := c2.When(false, func(c *Collection[int]) *Collection[int] { return c.Push(4) }) + if result2.Count() != 3 { t.Errorf("expected 3 items, got %d", result2.Count()) } @@ -740,6 +846,7 @@ func TestUnless(t *testing.T) { result := c.Unless(false, func(c *Collection[int]) *Collection[int] { return c.Push(4) }) + if result.Count() != 4 { t.Errorf("expected 4, got %d", result.Count()) } @@ -749,6 +856,7 @@ func TestDiff(t *testing.T) { c := New(1, 2, 3, 4, 5) result := Diff(c, []int{2, 4}) expected := []int{1, 3, 5} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -758,6 +866,7 @@ func TestIntersect(t *testing.T) { c := New(1, 2, 3, 4, 5) result := Intersect(c, []int{2, 4, 6}) expected := []int{2, 4} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -766,6 +875,7 @@ func TestIntersect(t *testing.T) { func TestDiffUsing(t *testing.T) { c := New(1, 2, 3) result := c.DiffUsing([]int{2, 3}, func(a, b int) bool { return a == b }) + if !reflect.DeepEqual(result.All(), []int{1}) { t.Errorf("expected [1], got %v", result.All()) } @@ -774,6 +884,7 @@ func TestDiffUsing(t *testing.T) { func TestIntersectUsing(t *testing.T) { c := New(1, 2, 3) result := c.IntersectUsing([]int{2, 3, 4}, func(a, b int) bool { return a == b }) + if !reflect.DeepEqual(result.All(), []int{2, 3}) { t.Errorf("expected [2 3], got %v", result.All()) } @@ -782,9 +893,11 @@ func TestIntersectUsing(t *testing.T) { func TestZip(t *testing.T) { c := New(1, 2, 3) result := Zip(c, []int{10, 20, 30}) + if len(result.All()) != 3 { t.Errorf("expected 3 pairs, got %d", len(result.All())) } + if !reflect.DeepEqual(result.All()[0], []int{1, 10}) { t.Errorf("expected [1 10], got %v", result.All()[0]) } @@ -793,6 +906,7 @@ func TestZip(t *testing.T) { func TestCrossJoin(t *testing.T) { c := New(1, 2) result := CrossJoin(c, []int{10, 20}) + if len(result.All()) != 4 { t.Errorf("expected 4 combinations, got %d", len(result.All())) } @@ -801,9 +915,11 @@ func TestCrossJoin(t *testing.T) { func TestCombine(t *testing.T) { keys := New("name", "age") result := Combine(keys, []string{"John", "30"}) + if result.Count() != 2 { t.Errorf("expected 2 pairs, got %d", result.Count()) } + if result.All()[0].Key != "name" || result.All()[0].Value != "John" { t.Error("unexpected pair values") } @@ -813,6 +929,7 @@ func TestCollapse(t *testing.T) { c := New([]int{1, 2}, []int{3, 4}, []int{5}) result := Collapse(c) expected := []int{1, 2, 3, 4, 5} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -823,9 +940,11 @@ func TestPluck(t *testing.T) { Name string Age int } + c := New(User{"Alice", 25}, User{"Bob", 30}) names := Pluck(c, func(u User) string { return u.Name }) expected := []string{"Alice", "Bob"} + if !reflect.DeepEqual(names.All(), expected) { t.Errorf("expected %v, got %v", expected, names.All()) } @@ -837,11 +956,14 @@ func TestGroupBy(t *testing.T) { if item%2 == 0 { return "even" } + return "odd" }) + if groups["even"].Count() != 3 { t.Errorf("expected 3 evens, got %d", groups["even"].Count()) } + if groups["odd"].Count() != 3 { t.Errorf("expected 3 odds, got %d", groups["odd"].Count()) } @@ -852,8 +974,10 @@ func TestKeyBy(t *testing.T) { ID int Name string } + c := New(User{1, "Alice"}, User{2, "Bob"}) keyed := KeyBy(c, func(u User) int { return u.ID }) + if keyed[1].Name != "Alice" { t.Error("expected Alice at key 1") } @@ -862,6 +986,7 @@ func TestKeyBy(t *testing.T) { func TestCountBy(t *testing.T) { c := New("apple", "banana", "apple", "cherry", "banana", "apple") counts := CountBy(c, func(item string) string { return item }) + if counts["apple"] != 3 { t.Errorf("expected 3 apples, got %d", counts["apple"]) } @@ -873,8 +998,10 @@ func TestMapToDictionary(t *testing.T) { if item%2 == 0 { return "even", item } + return "odd", item }) + if len(dict["even"]) != 2 { t.Errorf("expected 2 evens, got %d", len(dict["even"])) } @@ -885,8 +1012,10 @@ func TestMapWithKeys(t *testing.T) { ID int Name string } + c := New(User{1, "Alice"}, User{2, "Bob"}) result := MapWithKeys(c, func(u User) (int, string) { return u.ID, u.Name }) + if result[1] != "Alice" { t.Error("expected Alice at key 1") } @@ -896,6 +1025,7 @@ func TestOnly(t *testing.T) { c := New(10, 20, 30, 40, 50) result := c.Only(1, 3) expected := []int{20, 40} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -905,6 +1035,7 @@ func TestExcept(t *testing.T) { c := New(10, 20, 30, 40, 50) result := c.Except(1, 3) expected := []int{10, 30, 50} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -912,12 +1043,15 @@ func TestExcept(t *testing.T) { func TestHas(t *testing.T) { c := New(1, 2, 3) + if !c.Has(0) { t.Error("expected index 0 to exist") } + if !c.Has(-1) { t.Error("expected negative index to work") } + if c.Has(10) { t.Error("expected index 10 to not exist") } @@ -925,6 +1059,7 @@ func TestHas(t *testing.T) { func TestSum(t *testing.T) { c := New(1, 2, 3, 4, 5) + if Sum(c) != 15 { t.Errorf("expected 15, got %d", Sum(c)) } @@ -934,8 +1069,10 @@ func TestSumBy(t *testing.T) { type Item struct { Price float64 } + c := New(Item{10.5}, Item{20.5}, Item{30.0}) result := SumBy(c, func(i Item) float64 { return i.Price }) + if result != 61.0 { t.Errorf("expected 61.0, got %f", result) } @@ -944,6 +1081,7 @@ func TestSumBy(t *testing.T) { func TestAvg(t *testing.T) { c := New(1.0, 2.0, 3.0, 4.0, 5.0) result := Avg(c) + if result != 3.0 { t.Errorf("expected 3.0, got %f", result) } @@ -952,6 +1090,7 @@ func TestAvg(t *testing.T) { func TestMin(t *testing.T) { c := New(3, 1, 4, 1, 5, 9) v, ok := Min(c) + if !ok || v != 1 { t.Errorf("expected 1, got %d", v) } @@ -960,6 +1099,7 @@ func TestMin(t *testing.T) { func TestMax(t *testing.T) { c := New(3, 1, 4, 1, 5, 9) v, ok := Max(c) + if !ok || v != 9 { t.Errorf("expected 9, got %d", v) } @@ -968,12 +1108,14 @@ func TestMax(t *testing.T) { func TestMedian(t *testing.T) { c := New(1.0, 2.0, 3.0, 4.0, 5.0) result := Median(c) + if result != 3.0 { t.Errorf("expected 3.0, got %f", result) } c2 := New(1.0, 2.0, 3.0, 4.0) result2 := Median(c2) + if result2 != 2.5 { t.Errorf("expected 2.5, got %f", result2) } @@ -982,6 +1124,7 @@ func TestMedian(t *testing.T) { func TestMode(t *testing.T) { c := New(1, 2, 2, 3, 3, 3) result := Mode(c) + if len(result) != 1 || result[0] != 3 { t.Errorf("expected [3], got %v", result) } @@ -990,9 +1133,11 @@ func TestMode(t *testing.T) { func TestToJSON(t *testing.T) { c := New(1, 2, 3) b, err := c.ToJSON() + if err != nil { t.Fatal(err) } + if string(b) != "[1,2,3]" { t.Errorf("expected [1,2,3], got %s", string(b)) } @@ -1001,10 +1146,13 @@ func TestToJSON(t *testing.T) { func TestMarshalJSON(t *testing.T) { c := New("a", "b", "c") b, err := json.Marshal(c) + if err != nil { t.Fatal(err) } + expected := `["a","b","c"]` + if string(b) != expected { t.Errorf("expected %s, got %s", expected, string(b)) } @@ -1013,10 +1161,13 @@ func TestMarshalJSON(t *testing.T) { func TestUnmarshalJSON(t *testing.T) { c := Empty[int]() err := json.Unmarshal([]byte("[1,2,3]"), c) + if err != nil { t.Fatal(err) } + expected := []int{1, 2, 3} + if !reflect.DeepEqual(c.All(), expected) { t.Errorf("expected %v, got %v", expected, c.All()) } @@ -1026,6 +1177,7 @@ func TestCopy(t *testing.T) { c := New(1, 2, 3) c2 := c.Copy() c.Push(4) + if c2.Count() != 3 { t.Error("copy should not be affected by changes to original") } @@ -1034,9 +1186,11 @@ func TestCopy(t *testing.T) { func TestSplice(t *testing.T) { c := New(1, 2, 3, 4, 5) removed := c.Splice(1, 2) + if !reflect.DeepEqual(removed.All(), []int{2, 3}) { t.Errorf("expected [2 3], got %v", removed.All()) } + if !reflect.DeepEqual(c.All(), []int{1, 4, 5}) { t.Errorf("expected [1 4 5], got %v", c.All()) } @@ -1045,9 +1199,11 @@ func TestSplice(t *testing.T) { func TestSpliceReplace(t *testing.T) { c := New(1, 2, 3, 4, 5) removed := c.SpliceReplace(1, 2, []int{20, 30, 40}) + if !reflect.DeepEqual(removed.All(), []int{2, 3}) { t.Errorf("expected [2 3] removed, got %v", removed.All()) } + if !reflect.DeepEqual(c.All(), []int{1, 20, 30, 40, 4, 5}) { t.Errorf("expected [1 20 30 40 4 5], got %v", c.All()) } @@ -1057,6 +1213,7 @@ func TestWhere(t *testing.T) { c := New(1, 2, 3, 4, 5) result := c.Where(func(item int) bool { return item > 3 }) expected := []int{4, 5} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -1065,6 +1222,7 @@ func TestWhere(t *testing.T) { func TestRandom(t *testing.T) { c := New(1, 2, 3, 4, 5) result := c.Random(2) + if result.Count() != 2 { t.Errorf("expected 2 items, got %d", result.Count()) } @@ -1076,9 +1234,11 @@ func TestTap(t *testing.T) { result := c.Tap(func(c *Collection[int]) { tapped = true }) + if !tapped { t.Error("expected tap callback to be called") } + if result != c { t.Error("expected same collection returned") } @@ -1089,6 +1249,7 @@ func TestPipe(t *testing.T) { result := Pipe(c, func(c *Collection[int]) int { return Sum(c) }) + if result != 6 { t.Errorf("expected 6, got %d", result) } @@ -1105,6 +1266,7 @@ func TestPipeThrough(t *testing.T) { }, ) expected := []int{2, 3, 10} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -1115,8 +1277,10 @@ func TestMinBy(t *testing.T) { Name string Age int } + c := New(Item{"Bob", 30}, Item{"Alice", 25}, Item{"Charlie", 35}) v, ok := MinBy(c, func(item Item) int { return item.Age }) + if !ok || v.Name != "Alice" { t.Errorf("expected Alice, got %s", v.Name) } @@ -1127,8 +1291,10 @@ func TestMaxBy(t *testing.T) { Name string Age int } + c := New(Item{"Bob", 30}, Item{"Alice", 25}, Item{"Charlie", 35}) v, ok := MaxBy(c, func(item Item) int { return item.Age }) + if !ok || v.Name != "Charlie" { t.Errorf("expected Charlie, got %s", v.Name) } @@ -1140,6 +1306,7 @@ func TestMapInto(t *testing.T) { return string(rune('A' + v - 1)) }) expected := []string{"A", "B", "C"} + if !reflect.DeepEqual(result.All(), expected) { t.Errorf("expected %v, got %v", expected, result.All()) } @@ -1148,11 +1315,13 @@ func TestMapInto(t *testing.T) { func TestEnsure(t *testing.T) { c := New(2, 4, 6) err := c.Ensure(func(v int) bool { return v%2 == 0 }) + if err != nil { t.Errorf("expected no error, got %v", err) } err = c.Ensure(func(v int) bool { return v > 10 }) + if err == nil { t.Error("expected error") } @@ -1161,6 +1330,7 @@ func TestEnsure(t *testing.T) { func TestString(t *testing.T) { c := New(1, 2, 3) s := c.String() + if s != "[1,2,3]" { t.Errorf("expected [1,2,3], got %s", s) } @@ -1169,6 +1339,7 @@ func TestString(t *testing.T) { func TestDot(t *testing.T) { c := New(1, 2, 3) d := c.Dot() + if !reflect.DeepEqual(d.All(), c.All()) { t.Error("expected copy") } @@ -1178,6 +1349,7 @@ func TestSortDesc(t *testing.T) { c := New(1, 3, 2, 5, 4) sorted := c.SortDesc(func(a, b int) bool { return a < b }) expected := []int{5, 4, 3, 2, 1} + if !reflect.DeepEqual(sorted.All(), expected) { t.Errorf("expected %v, got %v", expected, sorted.All()) } @@ -1186,11 +1358,13 @@ func TestSortDesc(t *testing.T) { func TestValues(t *testing.T) { c := New(10, 20, 30) v := c.Values() + if !reflect.DeepEqual(v.All(), c.All()) { t.Error("expected same values") } // Ensure it's a copy v.Push(40) + if c.Count() != 3 { t.Error("values should return a copy") } @@ -1200,6 +1374,7 @@ func TestToSlice(t *testing.T) { c := New(1, 2, 3) s := c.ToSlice() expected := []int{1, 2, 3} + if !reflect.DeepEqual(s, expected) { t.Errorf("expected %v, got %v", expected, s) } @@ -1211,9 +1386,11 @@ func TestTapEach(t *testing.T) { result := c.TapEach(func(item int, _ int) { sum += item }) + if sum != 6 { t.Errorf("expected 6, got %d", sum) } + if result != c { t.Error("expected same collection") } @@ -1223,9 +1400,11 @@ func TestSplitIn(t *testing.T) { c := New(1, 2, 3, 4, 5, 6, 7) groups := c.SplitIn(3) total := 0 + for _, g := range groups { total += len(g) } + if total != 7 { t.Errorf("expected 7 total items, got %d", total) } @@ -1235,8 +1414,11 @@ func TestSplitIn(t *testing.T) { func TestModeTie(t *testing.T) { c := New(1, 1, 2, 2, 3) result := Mode(c) + sort.Ints(result) + expected := []int{1, 2} + if !reflect.DeepEqual(result, expected) { t.Errorf("expected %v, got %v", expected, result) } @@ -1247,6 +1429,7 @@ func TestWhenEmpty(t *testing.T) { result := empty.WhenEmpty(func(c *Collection[int]) *Collection[int] { return c.Push(1, 2, 3) }) + if result.Count() != 3 { t.Errorf("expected 3, got %d", result.Count()) } @@ -1257,9 +1440,754 @@ func TestWhenNotEmpty(t *testing.T) { called := false c.WhenNotEmpty(func(c *Collection[int]) *Collection[int] { called = true + return c }) + if !called { t.Error("expected callback to be called") } } + +func TestWrap(t *testing.T) { + c := New(1, 2, 3) + wrapped := Wrap[int](c) + + if wrapped != c { + t.Error("expected same collection") + } + + wrapped2 := Wrap[int]([]int{1, 2, 3}) + + if wrapped2.Count() != 3 { + t.Errorf("expected 3, got %d", wrapped2.Count()) + } + + wrapped3 := Wrap[int](42) + + if wrapped3.Count() != 1 { + t.Errorf("expected 1, got %d", wrapped3.Count()) + } + + wrapped4 := Wrap[int]("string") + + if !wrapped4.IsEmpty() { + t.Error("expected empty for incompatible type") + } +} + +func TestUnwrap(t *testing.T) { + c := New(1, 2, 3) + items := Unwrap[int](c) + + if len(items) != 3 { + t.Errorf("expected 3, got %d", len(items)) + } + + items2 := Unwrap[int]([]int{4, 5}) + + if len(items2) != 2 { + t.Errorf("expected 2, got %d", len(items2)) + } + + items3 := Unwrap[int]("incompatible") + + if items3 != nil { + t.Error("expected nil for incompatible type") + } +} + +func TestHasMany(t *testing.T) { + if !New(1, 2).HasMany() { + t.Error("expected true for 2+ items") + } + + if New(1).HasMany() { + t.Error("expected false for single item") + } + + if Empty[int]().HasMany() { + t.Error("expected false for empty") + } +} + +func TestLen(t *testing.T) { + c := New(1, 2, 3) + + if c.Len() != 3 { + t.Errorf("expected 3, got %d", c.Len()) + } +} + +func TestToBase(t *testing.T) { + c := New(1, 2, 3) + + if c.ToBase() != c { + t.Error("expected same collection") + } +} + +func TestIter(t *testing.T) { + c := New(10, 20, 30) + sum := 0 + + for item := range c.Iter() { + sum += item + } + + if sum != 60 { + t.Errorf("expected 60, got %d", sum) + } +} + +func TestPairIter(t *testing.T) { + c := New(10, 20, 30) + indexSum := 0 + valueSum := 0 + + for idx, item := range c.PairIter() { + indexSum += idx + valueSum += item + } + + if indexSum != 3 { + t.Errorf("expected index sum 3, got %d", indexSum) + } + + if valueSum != 60 { + t.Errorf("expected value sum 60, got %d", valueSum) + } +} + +func TestHasSole(t *testing.T) { + c := New(1, 2, 3) + + if !c.HasSole(func(item int, _ int) bool { return item == 2 }) { + t.Error("expected true for single match") + } + + if c.HasSole(func(item int, _ int) bool { return item > 1 }) { + t.Error("expected false for multiple matches") + } + + if c.HasSole(func(item int, _ int) bool { return item > 10 }) { + t.Error("expected false for no matches") + } + + single := New(42) + + if !single.HasSole() { + t.Error("expected true for single-item collection without predicate") + } +} + +func TestSomeCollection(t *testing.T) { + c := New(1, 2, 3) + + if !c.Some(func(item int, _ int) bool { return item == 2 }) { + t.Error("expected true") + } + + if c.Some(func(item int, _ int) bool { return item > 10 }) { + t.Error("expected false") + } +} + +func TestHasAny(t *testing.T) { + c := New(10, 20, 30) + + if !c.HasAny(0, 5) { + t.Error("expected true when at least one index valid") + } + + if c.HasAny(5, 6, 7) { + t.Error("expected false when all indices invalid") + } +} + +func TestUndot(t *testing.T) { + c := New(1, 2, 3) + c2 := c.Undot() + + if !reflect.DeepEqual(c.All(), c2.All()) { + t.Error("expected shallow copy") + } + + if c == c2 { + t.Error("expected different collection instance") + } +} + +func TestMedianBy(t *testing.T) { + type item struct { + val float64 + } + + c := Collect([]item{{10}, {20}, {30}}) + result := MedianBy(c, func(i item) float64 { return i.val }) + + if result != 20 { + t.Errorf("expected 20, got %f", result) + } +} + +func TestAvgBy(t *testing.T) { + type item struct { + val int + } + + c := Collect([]item{{10}, {20}, {30}}) + result := AvgBy(c, func(i item) int { return i.val }) + + if result != 20 { + t.Errorf("expected 20, got %f", result) + } + + empty := Empty[item]() + result = AvgBy(empty, func(i item) int { return i.val }) + + if result != 0 { + t.Errorf("expected 0 for empty, got %f", result) + } +} + +func TestAverage(t *testing.T) { + c := New(10, 20, 30) + + if Average(c) != 20 { + t.Errorf("expected 20, got %f", Average(c)) + } +} + +func TestAdd(t *testing.T) { + c := New(1, 2) + c.Add(3) + + if c.Count() != 3 { + t.Errorf("expected 3, got %d", c.Count()) + } + + if c.All()[2] != 3 { + t.Errorf("expected 3 at index 2, got %d", c.All()[2]) + } +} + +func TestUnshift(t *testing.T) { + c := New(2, 3) + c.Unshift(1) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(c.All(), expected) { + t.Errorf("expected %v, got %v", expected, c.All()) + } +} + +func TestMerge(t *testing.T) { + c := New(1, 2, 3) + result := c.Merge([]int{4, 5}) + + if result.Count() != 5 { + t.Errorf("expected 5, got %d", result.Count()) + } +} + +func TestMapToGroups(t *testing.T) { + type item struct { + cat string + value int + } + + c := Collect([]item{{"a", 1}, {"a", 2}, {"b", 3}}) + groups := MapToGroups(c, func(i item) (string, int) { + return i.cat, i.value + }) + + if len(groups["a"]) != 2 { + t.Errorf("expected 2 in group 'a', got %d", len(groups["a"])) + } + + if len(groups["b"]) != 1 { + t.Errorf("expected 1 in group 'b', got %d", len(groups["b"])) + } +} + +func TestEachSpread(t *testing.T) { + c := New(1, 2, 3) + count := 0 + c.EachSpread(func(item int, _ int) bool { + count++ + + return true + }) + + if count != 3 { + t.Errorf("expected 3, got %d", count) + } +} + +func TestPipeInto(t *testing.T) { + c := New(1, 2, 3) + result := PipeInto(c, func(col *Collection[int]) int { + return col.Count() + }) + + if result != 3 { + t.Errorf("expected 3, got %d", result) + } +} + +func TestUnlessEmpty(t *testing.T) { + c := New(1, 2) + result := c.UnlessEmpty(func(col *Collection[int]) *Collection[int] { + col.Push(3) + + return col + }) + + if result.Count() != 3 { + t.Errorf("expected 3, got %d", result.Count()) + } + + empty := Empty[int]() + called := false + empty.UnlessEmpty(func(col *Collection[int]) *Collection[int] { + called = true + + return col + }) + + if called { + t.Error("expected callback not called for empty collection") + } +} + +func TestUnlessNotEmpty(t *testing.T) { + empty := Empty[int]() + result := empty.UnlessNotEmpty(func(col *Collection[int]) *Collection[int] { + return col.Push(1, 2, 3) + }) + + if result.Count() != 3 { + t.Errorf("expected 3, got %d", result.Count()) + } + + c := New(1, 2) + called := false + c.UnlessNotEmpty(func(col *Collection[int]) *Collection[int] { + called = true + + return col + }) + + if called { + t.Error("expected callback not called for non-empty collection") + } +} + +func TestToPrettyJSON(t *testing.T) { + c := New(1, 2, 3) + b, err := c.ToPrettyJSON() + + if err != nil { + t.Fatal(err) + } + + s := string(b) + + if !strings.Contains(s, "\n") { + t.Error("expected pretty JSON with newlines") + } + + if !strings.Contains(s, " ") { + t.Error("expected 4-space indentation") + } +} + +func TestFlatten(t *testing.T) { + c := New(1, 2, 3) + flat := c.Flatten() + + if !reflect.DeepEqual(c.All(), flat.All()) { + t.Errorf("expected %v, got %v", c.All(), flat.All()) + } + + if c == flat { + t.Error("expected different collection instance") + } +} + +func TestDump(t *testing.T) { + c := New(1, 2, 3) + result := c.Dump() + + if result != c { + t.Error("expected Dump to return self") + } +} + +func TestLastEmpty(t *testing.T) { + c := Empty[int]() + + _, ok := c.Last() + + if ok { + t.Error("expected false for empty collection") + } + + _, ok = c.Last(func(item int, _ int) bool { return true }) + + if ok { + t.Error("expected false for empty collection with predicate") + } +} + +func TestLastNoMatch(t *testing.T) { + c := New(1, 2, 3) + + _, ok := c.Last(func(item int, _ int) bool { return item > 10 }) + + if ok { + t.Error("expected false when no match") + } +} + +func TestPopEmpty(t *testing.T) { + c := Empty[int]() + + _, ok := c.Pop() + + if ok { + t.Error("expected false for empty collection") + } +} + +func TestPopWithCount(t *testing.T) { + c := New(1, 2, 3, 4, 5) + item, ok := c.Pop(2) + + if !ok { + t.Error("expected true") + } + + if item != 5 { + t.Errorf("expected 5, got %d", item) + } +} + +func TestPopManyExceedsLen(t *testing.T) { + c := New(1, 2, 3) + popped := c.PopMany(10) + + if popped.Count() != 3 { + t.Errorf("expected 3 popped items, got %d", popped.Count()) + } + + if c.Count() != 0 { + t.Errorf("expected 0 remaining, got %d", c.Count()) + } +} + +func TestShiftEmpty(t *testing.T) { + c := Empty[int]() + + _, ok := c.Shift() + + if ok { + t.Error("expected false for empty collection") + } +} + +func TestShiftManyExceedsLen(t *testing.T) { + c := New(1, 2, 3) + shifted := c.ShiftMany(10) + + if shifted.Count() != 3 { + t.Errorf("expected 3 shifted items, got %d", shifted.Count()) + } + + if c.Count() != 0 { + t.Errorf("expected 0 remaining, got %d", c.Count()) + } +} + +func TestAvgEmpty(t *testing.T) { + c := Empty[int]() + + if Avg(c) != 0 { + t.Errorf("expected 0 for empty, got %f", Avg(c)) + } +} + +func TestMinEmpty(t *testing.T) { + c := Empty[int]() + + _, ok := Min(c) + + if ok { + t.Error("expected false for empty collection") + } +} + +func TestMaxEmpty(t *testing.T) { + c := Empty[int]() + + _, ok := Max(c) + + if ok { + t.Error("expected false for empty collection") + } +} + +func TestWhenFalseWithDefault(t *testing.T) { + c := New(1, 2, 3) + result := c.When(false, func(col *Collection[int]) *Collection[int] { + col.Push(4) + + return col + }, func(col *Collection[int]) *Collection[int] { + col.Push(99) + + return col + }) + + if !reflect.DeepEqual(result.All(), []int{1, 2, 3, 99}) { + t.Errorf("expected default callback applied, got %v", result.All()) + } +} + +func TestNewNil(t *testing.T) { + c := New[int]() + + if c.Count() != 0 { + t.Errorf("expected 0, got %d", c.Count()) + } +} + +func TestCollectNil(t *testing.T) { + c := Collect[int](nil) + + if c.Count() != 0 { + t.Errorf("expected 0, got %d", c.Count()) + } +} + +func TestFlip(t *testing.T) { + c := New(1, 2, 3) + flipped := c.Flip() + expected := []int{3, 2, 1} + + if !reflect.DeepEqual(flipped.All(), expected) { + t.Errorf("expected %v, got %v", expected, flipped.All()) + } +} + +func TestKeys(t *testing.T) { + c := New(10, 20, 30) + keys := c.Keys() + expected := []int{0, 1, 2} + + if !reflect.DeepEqual(keys.All(), expected) { + t.Errorf("expected %v, got %v", expected, keys.All()) + } +} + +func TestWhereNot(t *testing.T) { + c := New(1, 2, 3, 4, 5) + result := c.WhereNot(func(item int) bool { return item > 3 }) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestWhereNull(t *testing.T) { + c := New(0, 1, 0, 2, 0) + result := WhereNull(c, func(item int) int { return item }) + + if result.Count() != 3 { + t.Errorf("expected 3 zero values, got %d", result.Count()) + } +} + +func TestWhereNotNull(t *testing.T) { + c := New(0, 1, 0, 2, 0) + result := WhereNotNull(c, func(item int) int { return item }) + + if result.Count() != 2 { + t.Errorf("expected 2 non-zero values, got %d", result.Count()) + } +} + +func TestWhereIn(t *testing.T) { + c := New(1, 2, 3, 4, 5) + result := WhereIn(c, func(item int) int { return item }, []int{2, 4}) + expected := []int{2, 4} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestWhereNotIn(t *testing.T) { + c := New(1, 2, 3, 4, 5) + result := WhereNotIn(c, func(item int) int { return item }, []int{2, 4}) + expected := []int{1, 3, 5} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestWhereBetween(t *testing.T) { + c := New(1, 2, 3, 4, 5) + result := WhereBetween(c, func(item int) int { return item }, 2, 4) + expected := []int{2, 3, 4} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestWhereNotBetween(t *testing.T) { + c := New(1, 2, 3, 4, 5) + result := WhereNotBetween(c, func(item int) int { return item }, 2, 4) + expected := []int{1, 5} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestBeforeNoMatch(t *testing.T) { + c := New(1, 2, 3) + + _, ok := c.Before(func(item int, _ int) bool { return item > 10 }) + + if ok { + t.Error("expected false when no match") + } +} + +func TestBeforeFirstItem(t *testing.T) { + c := New(1, 2, 3) + + _, ok := c.Before(func(item int, _ int) bool { return item == 1 }) + + if ok { + t.Error("expected false when match is first item") + } +} + +func TestAfterNoMatch(t *testing.T) { + c := New(1, 2, 3) + + _, ok := c.After(func(item int, _ int) bool { return item > 10 }) + + if ok { + t.Error("expected false when no match") + } +} + +func TestAfterLastItem(t *testing.T) { + c := New(1, 2, 3) + + _, ok := c.After(func(item int, _ int) bool { return item == 3 }) + + if ok { + t.Error("expected false when match is last item") + } +} + +func TestSpliceNegativeOffset(t *testing.T) { + c := New(1, 2, 3, 4, 5) + removed := c.Splice(-2) + + if removed.Count() != 2 { + t.Errorf("expected 2 removed, got %d", removed.Count()) + } +} + +func TestSpliceOutOfBounds(t *testing.T) { + c := New(1, 2, 3) + removed := c.Splice(10) + + if !removed.IsEmpty() { + t.Error("expected empty result for out-of-bounds offset") + } +} + +func TestSpliceReplaceNegativeOffset(t *testing.T) { + c := New(1, 2, 3, 4, 5) + removed := c.SpliceReplace(-2, 2, []int{8, 9}) + + if removed.Count() != 2 { + t.Errorf("expected 2 removed, got %d", removed.Count()) + } +} + +func TestWhenFuncFalseWithDefault(t *testing.T) { + result := WhenFunc(false, func() int { + return 42 + }, func() int { + return 99 + }) + + if result != 99 { + t.Errorf("expected 99, got %d", result) + } +} + +func TestWhenFuncFalseNoDefault(t *testing.T) { + result := WhenFunc(false, func() int { + return 42 + }) + + if result != 0 { + t.Errorf("expected 0, got %d", result) + } +} + +func TestPullInvalid(t *testing.T) { + c := New(1, 2, 3) + _, ok := c.Pull(-1) + + if ok { + t.Error("expected false for invalid index") + } +} + +func TestForgetInvalid(t *testing.T) { + c := New(1, 2, 3) + result := c.Forget(-1) + + if result.Count() != 3 { + t.Errorf("expected unchanged, got %d items", result.Count()) + } +} + +func TestZipUnevenLengths(t *testing.T) { + c := New(1, 2) + result := Zip(c, []int{10, 20, 30}) + + if result.Count() != 3 { + t.Errorf("expected 3, got %d", result.Count()) + } +} + +func TestTimesZero(t *testing.T) { + c := Times(0, func(i int) int { return i }) + + if !c.IsEmpty() { + t.Error("expected empty for 0 times") + } +} + +func TestMultiplyZero(t *testing.T) { + c := New(1, 2, 3) + result := c.Multiply(0) + + if !result.IsEmpty() { + t.Error("expected empty for multiply 0") + } +} diff --git a/collection/combine.go b/collection/combine.go new file mode 100644 index 0000000..56d968b --- /dev/null +++ b/collection/combine.go @@ -0,0 +1,241 @@ +package collection + +import "github.com/gocanto/collection/support" + +// Diff returns the items in the collection that are not present in the given slice. +func Diff[T comparable](c *Collection[T], items []T) *Collection[T] { + lookup := make(map[T]bool, len(items)) + + for _, item := range items { + lookup[item] = true + } + + result := make([]T, 0) + + for _, item := range c.items { + if !lookup[item] { + result = append(result, item) + } + } + + return Collect(result) +} + +// DiffUsing returns items not present in the given slice, using a custom equality function. +func (c *Collection[T]) DiffUsing(items []T, equals func(T, T) bool) *Collection[T] { + result := make([]T, 0) + + for _, item := range c.items { + found := false + + for _, other := range items { + if equals(item, other) { + found = true + + break + } + } + + if !found { + result = append(result, item) + } + } + + return Collect(result) +} + +// Intersect returns the items present in both the collection and the given slice. +func Intersect[T comparable](c *Collection[T], items []T) *Collection[T] { + lookup := make(map[T]bool, len(items)) + + for _, item := range items { + lookup[item] = true + } + + result := make([]T, 0) + + for _, item := range c.items { + if lookup[item] { + result = append(result, item) + } + } + + return Collect(result) +} + +// IntersectUsing returns items present in both the collection and the given slice, +// using a custom equality function. +func (c *Collection[T]) IntersectUsing(items []T, equals func(T, T) bool) *Collection[T] { + result := make([]T, 0) + + for _, item := range c.items { + for _, other := range items { + if equals(item, other) { + result = append(result, item) + + break + } + } + } + + return Collect(result) +} + +// Zip merges the collection with each of the given slices element-by-element. +func Zip[T any](c *Collection[T], others ...[]T) *Collection[[]T] { + maxLen := len(c.items) + + for _, o := range others { + if len(o) > maxLen { + maxLen = len(o) + } + } + + result := make([][]T, maxLen) + + for i := 0; i < maxLen; i++ { + group := make([]T, 0, 1+len(others)) + + if i < len(c.items) { + group = append(group, c.items[i]) + } else { + var zero T + + group = append(group, zero) + } + + for _, o := range others { + if i < len(o) { + group = append(group, o[i]) + } else { + var zero T + + group = append(group, zero) + } + } + + result[i] = group + } + + return Collect(result) +} + +// CrossJoin returns the cross-product of the collection with the given slices. +func CrossJoin[T any](c *Collection[T], others ...[]T) *Collection[[]T] { + results := [][]T{{}} + allLists := append([][]T{c.items}, others...) + + for _, list := range allLists { + var newResults [][]T + + for _, result := range results { + for _, item := range list { + newResult := make([]T, len(result)+1) + copy(newResult, result) + newResult[len(result)] = item + newResults = append(newResults, newResult) + } + } + + results = newResults + } + + return Collect(results) +} + +// Combine pairs of keys from this collection with values from the given slice, +// returning a collection of Pair values. +func Combine[K any, V any](keys *Collection[K], values []V) *Collection[support.Pair[K, V]] { + minLen := len(keys.items) + + if len(values) < minLen { + minLen = len(values) + } + + result := make([]support.Pair[K, V], minLen) + + for i := 0; i < minLen; i++ { + result[i] = support.Pair[K, V]{Key: keys.items[i], Value: values[i]} + } + + return Collect(result) +} + +// Collapse flattens a collection of slices into a single, flat collection. +func Collapse[T any](c *Collection[[]T]) *Collection[T] { + result := make([]T, 0) + + for _, items := range c.items { + result = append(result, items...) + } + + return Collect(result) +} + +// GroupBy groups the collection's items by a key returned from the given function. +func GroupBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]*Collection[T] { + groups := make(map[K]*Collection[T]) + + for _, item := range c.items { + key := keyFunc(item) + + if _, ok := groups[key]; !ok { + groups[key] = Empty[T]() + } + + groups[key].Push(item) + } + + return groups +} + +// KeyBy indexes the collection by a key returned from the given function. +func KeyBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]T { + result := make(map[K]T) + + for _, item := range c.items { + result[keyFunc(item)] = item + } + + return result +} + +// CountBy counts how many items produce each key from the given function. +func CountBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]int { + result := make(map[K]int) + + for _, item := range c.items { + result[keyFunc(item)]++ + } + + return result +} + +// MapToDictionary maps each item to a key-value pair and groups values by key. +func MapToDictionary[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K][]V { + result := make(map[K][]V) + + for _, item := range c.items { + key, value := callback(item) + result[key] = append(result[key], value) + } + + return result +} + +// MapToGroups is an alias for MapToDictionary. +func MapToGroups[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K][]V { + return MapToDictionary(c, callback) +} + +// MapWithKeys maps each item to a key-value pair, returning a map. +func MapWithKeys[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K]V { + result := make(map[K]V) + + for _, item := range c.items { + key, value := callback(item) + result[key] = value + } + + return result +} diff --git a/collection/doc.go b/collection/doc.go index c524f81..8665b9c 100644 --- a/collection/doc.go +++ b/collection/doc.go @@ -1,21 +1,15 @@ -// Package collection provides fluent, generic wrappers for working with slices and maps. +// Package collection provides a fluent, generic wrapper for working with slices of data. // -// The core types are: +// The core type is: // // - [Collection] β€” a generic wrapper around a slice with a rich set of chainable methods // for filtering, sorting, transforming, and aggregating data. -// - [MapCollection] β€” an ordered map with a fluent API for key-value operations, -// preserving insertion order. -// - [LazyCollection] β€” a lazily-evaluated sequence backed by [iter.Seq], allowing -// efficient pipeline processing of large or infinite datasets. // -// All three types support conversion to Go 1.23+ iterators via their Iter methods, -// enabling use with range-over-func loops. +// # Related packages // -// # Sub-packages -// -// Standalone utility functions are available in sub-packages: -// -// - [github.com/gocanto/collection/arr] β€” generic slice helpers (filter, sort, partition, etc.) -// - [github.com/gocanto/collection/kv] β€” map helpers with dot-notation support for nested maps +// - [github.com/gocanto/collection/lazy] β€” lazily evaluated sequences backed by iter.Seq +// - [github.com/gocanto/collection/collectible] β€” ordered map with fluent key-value API +// - [github.com/gocanto/collection/support] β€” shared types (Pair, Numeric) and errors +// - [github.com/gocanto/collection/arr] β€” generic slice helpers +// - [github.com/gocanto/collection/kv] β€” map helpers with dot-notation support package collection diff --git a/collection/helpers.go b/collection/helpers.go index 61b3ef2..912d7d3 100644 --- a/collection/helpers.go +++ b/collection/helpers.go @@ -15,8 +15,10 @@ func ValueFunc[T any](callback func() T) T { func Head[T any](items []T) (T, bool) { if len(items) == 0 { var zero T + return zero, false } + return items[0], true } @@ -25,8 +27,10 @@ func Head[T any](items []T) (T, bool) { func Last[T any](items []T) (T, bool) { if len(items) == 0 { var zero T + return zero, false } + return items[len(items)-1], true } @@ -36,10 +40,13 @@ func WhenValue[T any](condition bool, value T, defaults ...T) T { if condition { return value } + if len(defaults) > 0 { return defaults[0] } + var zero T + return zero } @@ -49,9 +56,12 @@ func WhenFunc[T any](condition bool, callback func() T, defaults ...func() T) T if condition { return callback() } + if len(defaults) > 0 { return defaults[0]() } + var zero T + return zero } diff --git a/collection/helpers_test.go b/collection/helpers_test.go index b413637..c7a60df 100644 --- a/collection/helpers_test.go +++ b/collection/helpers_test.go @@ -6,6 +6,7 @@ import ( func TestValue(t *testing.T) { v := Value(42) + if v != 42 { t.Errorf("expected 42, got %d", v) } @@ -13,6 +14,7 @@ func TestValue(t *testing.T) { func TestValueFunc(t *testing.T) { v := ValueFunc(func() int { return 42 }) + if v != 42 { t.Errorf("expected 42, got %d", v) } @@ -20,11 +22,13 @@ func TestValueFunc(t *testing.T) { func TestHead(t *testing.T) { v, ok := Head([]int{1, 2, 3}) + if !ok || v != 1 { t.Errorf("expected 1, got %d", v) } _, ok = Head([]int{}) + if ok { t.Error("expected not found for empty slice") } @@ -32,11 +36,13 @@ func TestHead(t *testing.T) { func TestLast_Helper(t *testing.T) { v, ok := Last([]int{1, 2, 3}) + if !ok || v != 3 { t.Errorf("expected 3, got %d", v) } _, ok = Last([]int{}) + if ok { t.Error("expected not found for empty slice") } @@ -44,16 +50,19 @@ func TestLast_Helper(t *testing.T) { func TestWhenValue(t *testing.T) { v := WhenValue(true, "yes", "no") + if v != "yes" { t.Errorf("expected 'yes', got '%s'", v) } v = WhenValue(false, "yes", "no") + if v != "no" { t.Errorf("expected 'no', got '%s'", v) } v = WhenValue(false, "yes") + if v != "" { t.Errorf("expected zero value, got '%s'", v) } @@ -61,11 +70,13 @@ func TestWhenValue(t *testing.T) { func TestWhenFunc(t *testing.T) { v := WhenFunc(true, func() int { return 42 }, func() int { return 0 }) + if v != 42 { t.Errorf("expected 42, got %d", v) } v = WhenFunc(false, func() int { return 42 }, func() int { return 0 }) + if v != 0 { t.Errorf("expected 0, got %d", v) } diff --git a/collection/lazy_collection.go b/collection/lazy_collection.go deleted file mode 100644 index c223914..0000000 --- a/collection/lazy_collection.go +++ /dev/null @@ -1,842 +0,0 @@ -package collection - -import ( - "fmt" - "iter" - "strings" - "time" -) - -// LazyCollection provides lazy evaluation for collection operations. -// Items are only computed when needed, making it memory-efficient for large datasets. -type LazyCollection[T any] struct { - source iter.Seq[T] -} - -// NewLazy creates a new LazyCollection from an iterator function. -// The source receives a yield function; call yield for each item and -// stop generating if yield returns false. -func NewLazy[T any](source iter.Seq[T]) *LazyCollection[T] { - return &LazyCollection[T]{source: source} -} - -// LazyFrom creates a LazyCollection from a slice. -func LazyFrom[T any](items []T) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - for _, item := range items { - if !yield(item) { - return - } - } - }) -} - -// LazyEmpty creates an empty LazyCollection. -func LazyEmpty[T any]() *LazyCollection[T] { - return NewLazy[T](func(yield func(T) bool) {}) -} - -// LazyRange creates a lazy collection of sequential integers from start to end (inclusive). -// If from > to, the sequence counts downward. -func LazyRange(from, to int) *LazyCollection[int] { - return NewLazy(func(yield func(int) bool) { - if from <= to { - for i := from; i <= to; i++ { - if !yield(i) { - return - } - } - } else { - for i := from; i >= to; i-- { - if !yield(i) { - return - } - } - } - }) -} - -// LazyTimes creates a lazy collection by invoking the callback n times. -// The callback receives a 1-based index. -func LazyTimes[T any](number int, callback func(int) T) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - for i := 1; i <= number; i++ { - if !yield(callback(i)) { - return - } - } - }) -} - -// Iter returns the underlying iterator for use with range loops. -func (lc *LazyCollection[T]) Iter() iter.Seq[T] { - return lc.source -} - -// All eagerly evaluates the lazy collection and returns all items as a slice. -func (lc *LazyCollection[T]) All() []T { - result := make([]T, 0) - lc.source(func(item T) bool { - result = append(result, item) - return true - }) - return result -} - -// Eager eagerly evaluates the lazy collection and returns a Collection. -func (lc *LazyCollection[T]) Eager() *Collection[T] { - return Collect(lc.All()) -} - -// Collect converts the lazy collection to an eager Collection. -func (lc *LazyCollection[T]) Collect() *Collection[T] { - return lc.Eager() -} - -// Count returns the total number of items by eagerly evaluating the collection. -func (lc *LazyCollection[T]) Count() int { - count := 0 - lc.source(func(_ T) bool { - count++ - return true - }) - return count -} - -// IsEmpty reports whether the lazy collection contains no items. -func (lc *LazyCollection[T]) IsEmpty() bool { - empty := true - lc.source(func(_ T) bool { - empty = false - return false - }) - return empty -} - -// IsNotEmpty reports whether the lazy collection contains at least one item. -func (lc *LazyCollection[T]) IsNotEmpty() bool { - return !lc.IsEmpty() -} - -// ContainsOneItem reports whether the lazy collection contains exactly one item. -func (lc *LazyCollection[T]) ContainsOneItem() bool { - return lc.Count() == 1 -} - -// ContainsManyItems reports whether the lazy collection contains more than one item. -func (lc *LazyCollection[T]) ContainsManyItems() bool { - return lc.Count() > 1 -} - -// First returns the first element matching the optional predicate. -// If no predicate is given, it returns the first element. -// The second return value indicates whether a matching element was found. -func (lc *LazyCollection[T]) First(predicates ...func(T, int) bool) (T, bool) { - var result T - found := false - idx := 0 - if len(predicates) == 0 || predicates[0] == nil { - lc.source(func(item T) bool { - result = item - found = true - return false - }) - } else { - predicate := predicates[0] - lc.source(func(item T) bool { - if predicate(item, idx) { - result = item - found = true - return false - } - idx++ - return true - }) - } - return result, found -} - -// FirstOrFail returns the first element matching the optional predicate, -// or an ItemNotFoundError if no element is found. -func (lc *LazyCollection[T]) FirstOrFail(predicates ...func(T, int) bool) (T, error) { - item, ok := lc.First(predicates...) - if !ok { - var zero T - return zero, &ItemNotFoundError{} - } - return item, nil -} - -// Last returns the last element matching the optional predicate. -// The second return value indicates whether a matching element was found. -func (lc *LazyCollection[T]) Last(predicates ...func(T, int) bool) (T, bool) { - var result T - found := false - idx := 0 - if len(predicates) == 0 || predicates[0] == nil { - lc.source(func(item T) bool { - result = item - found = true - return true - }) - } else { - predicate := predicates[0] - lc.source(func(item T) bool { - if predicate(item, idx) { - result = item - found = true - } - idx++ - return true - }) - } - return result, found -} - -// Sole returns the only element matching the optional predicate. -// It returns an error if zero or more than one element matches. -func (lc *LazyCollection[T]) Sole(predicates ...func(T, int) bool) (T, error) { - return lc.Eager().Sole(predicates...) -} - -// Get returns the item at the given zero-based index. -// The second return value indicates whether the index exists. -func (lc *LazyCollection[T]) Get(index int) (T, bool) { - var result T - found := false - idx := 0 - lc.source(func(item T) bool { - if idx == index { - result = item - found = true - return false - } - idx++ - return true - }) - return result, found -} - -// Contains reports whether any item satisfies the predicate. -func (lc *LazyCollection[T]) Contains(predicate func(T, int) bool) bool { - found := false - idx := 0 - lc.source(func(item T) bool { - if predicate(item, idx) { - found = true - return false - } - idx++ - return true - }) - return found -} - -// Some is an alias for Contains. -func (lc *LazyCollection[T]) Some(predicate func(T, int) bool) bool { - return lc.Contains(predicate) -} - -// DoesntContain reports whether no item satisfies the predicate. -func (lc *LazyCollection[T]) DoesntContain(predicate func(T, int) bool) bool { - return !lc.Contains(predicate) -} - -// Search returns the index of the first item satisfying the predicate. -// The second return value indicates whether a match was found. -func (lc *LazyCollection[T]) Search(predicate func(T, int) bool) (int, bool) { - resultIdx := -1 - found := false - idx := 0 - lc.source(func(item T) bool { - if predicate(item, idx) { - resultIdx = idx - found = true - return false - } - idx++ - return true - }) - return resultIdx, found -} - -// Before returns the item immediately before the first item matching the predicate. -// The second return value indicates whether such an item exists. -func (lc *LazyCollection[T]) Before(predicate func(T, int) bool) (T, bool) { - var prev T - hasPrev := false - found := false - idx := 0 - lc.source(func(item T) bool { - if predicate(item, idx) { - found = true - return false - } - prev = item - hasPrev = true - idx++ - return true - }) - if found && hasPrev { - return prev, true - } - var zero T - return zero, false -} - -// After returns the item immediately after the first item matching the predicate. -// The second return value indicates whether such an item exists. -func (lc *LazyCollection[T]) After(predicate func(T, int) bool) (T, bool) { - matched := false - var result T - found := false - idx := 0 - lc.source(func(item T) bool { - if matched { - result = item - found = true - return false - } - if predicate(item, idx) { - matched = true - } - idx++ - return true - }) - return result, found -} - -// Each iterates over items, calling the callback for each one. -// If the callback returns false, iteration stops early. -func (lc *LazyCollection[T]) Each(callback func(T, int) bool) *LazyCollection[T] { - idx := 0 - lc.source(func(item T) bool { - cont := callback(item, idx) - idx++ - return cont - }) - return lc -} - -// Tap passes the lazy collection to the given callback and returns it unchanged. -func (lc *LazyCollection[T]) Tap(callback func(*LazyCollection[T])) *LazyCollection[T] { - callback(lc) - return lc -} - -// Filter returns a new lazy collection containing only items for which the callback returns true. -func (lc *LazyCollection[T]) Filter(callback func(T, int) bool) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - idx := 0 - lc.source(func(item T) bool { - if callback(item, idx) { - if !yield(item) { - return false - } - } - idx++ - return true - }) - }) -} - -// Reject returns a new lazy collection excluding items for which the callback returns true. -func (lc *LazyCollection[T]) Reject(callback func(T, int) bool) *LazyCollection[T] { - return lc.Filter(func(item T, index int) bool { - return !callback(item, index) - }) -} - -// LazyMap transforms each item using the callback, returning a new lazy collection. -func LazyMap[T any, R any](lc *LazyCollection[T], callback func(T, int) R) *LazyCollection[R] { - return NewLazy(func(yield func(R) bool) { - idx := 0 - lc.source(func(item T) bool { - if !yield(callback(item, idx)) { - return false - } - idx++ - return true - }) - }) -} - -// LazyFlatMap transforms each item into a slice and flattens the results into a new lazy collection. -func LazyFlatMap[T any, R any](lc *LazyCollection[T], callback func(T, int) []R) *LazyCollection[R] { - return NewLazy(func(yield func(R) bool) { - idx := 0 - lc.source(func(item T) bool { - for _, r := range callback(item, idx) { - if !yield(r) { - return false - } - } - idx++ - return true - }) - }) -} - -// Take returns a new lazy collection with at most limit items. -// A negative limit takes from the end (requires eager evaluation). -func (lc *LazyCollection[T]) Take(limit int) *LazyCollection[T] { - if limit < 0 { - // For negative take, we need to eagerly evaluate - items := lc.All() - start := len(items) + limit - if start < 0 { - start = 0 - } - return LazyFrom(items[start:]) - } - return NewLazy(func(yield func(T) bool) { - count := 0 - lc.source(func(item T) bool { - if count >= limit { - return false - } - count++ - return yield(item) - }) - }) -} - -// TakeUntil returns items from the beginning until the callback returns true. -func (lc *LazyCollection[T]) TakeUntil(callback func(T, int) bool) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - idx := 0 - lc.source(func(item T) bool { - if callback(item, idx) { - return false - } - idx++ - return yield(item) - }) - }) -} - -// TakeWhile returns items from the beginning while the callback returns true. -func (lc *LazyCollection[T]) TakeWhile(callback func(T, int) bool) *LazyCollection[T] { - return lc.TakeUntil(func(item T, idx int) bool { - return !callback(item, idx) - }) -} - -// TakeUntilTimeout returns items until the given duration has elapsed. -func (lc *LazyCollection[T]) TakeUntilTimeout(timeout time.Duration) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - deadline := time.Now().Add(timeout) - lc.source(func(item T) bool { - if time.Now().After(deadline) { - return false - } - return yield(item) - }) - }) -} - -// Skip returns a new lazy collection that skips the first count items. -func (lc *LazyCollection[T]) Skip(count int) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - skipped := 0 - lc.source(func(item T) bool { - if skipped < count { - skipped++ - return true - } - return yield(item) - }) - }) -} - -// SkipUntil skips items until the callback returns true, then yields the rest. -func (lc *LazyCollection[T]) SkipUntil(callback func(T, int) bool) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - found := false - idx := 0 - lc.source(func(item T) bool { - if !found && callback(item, idx) { - found = true - } - if found { - return yield(item) - } - idx++ - return true - }) - }) -} - -// SkipWhile skips items while the callback returns true, then yields the rest. -func (lc *LazyCollection[T]) SkipWhile(callback func(T, int) bool) *LazyCollection[T] { - return lc.SkipUntil(func(item T, idx int) bool { - return !callback(item, idx) - }) -} - -// Slice returns a subset of the lazy collection starting at offset with an optional length. -func (lc *LazyCollection[T]) Slice(offset int, lengths ...int) *LazyCollection[T] { - result := lc.Skip(offset) - if len(lengths) > 0 { - result = result.Take(lengths[0]) - } - return result -} - -// Chunk eagerly evaluates the lazy collection and splits items into groups of the given size. -func (lc *LazyCollection[T]) Chunk(size int) [][]T { - var result [][]T - chunk := make([]T, 0, size) - lc.source(func(item T) bool { - chunk = append(chunk, item) - if len(chunk) >= size { - result = append(result, chunk) - chunk = make([]T, 0, size) - } - return true - }) - if len(chunk) > 0 { - result = append(result, chunk) - } - return result -} - -// ChunkWhile splits the lazy collection into groups where consecutive items satisfy the callback. -// A new chunk is started when the callback returns false. -func (lc *LazyCollection[T]) ChunkWhile(callback func(T, int, []T) bool) [][]T { - var result [][]T - current := make([]T, 0) - idx := 0 - lc.source(func(item T) bool { - if len(current) == 0 || callback(item, idx, current) { - current = append(current, item) - } else { - result = append(result, current) - current = []T{item} - } - idx++ - return true - }) - if len(current) > 0 { - result = append(result, current) - } - return result -} - -// Nth returns every step-th element, starting from an optional offset. -func (lc *LazyCollection[T]) Nth(step int, offsets ...int) *LazyCollection[T] { - offset := 0 - if len(offsets) > 0 { - offset = offsets[0] - } - return NewLazy(func(yield func(T) bool) { - idx := 0 - lc.source(func(item T) bool { - if idx >= offset && (idx-offset)%step == 0 { - if !yield(item) { - return false - } - } - idx++ - return true - }) - }) -} - -// Concat returns a new lazy collection with the given items appended. -func (lc *LazyCollection[T]) Concat(items []T) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - lc.source(func(item T) bool { - return yield(item) - }) - for _, item := range items { - if !yield(item) { - return - } - } - }) -} - -// Pad returns a new lazy collection padded to the given size with the specified value. -// A negative size pads at the beginning; a positive size pads at the end. -func (lc *LazyCollection[T]) Pad(size int, value T) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - count := 0 - absSize := size - if absSize < 0 { - absSize = -absSize - } - if size < 0 { - // Evaluate to know count - items := lc.All() - padCount := absSize - len(items) - if padCount > 0 { - for i := 0; i < padCount; i++ { - if !yield(value) { - return - } - } - } - for _, item := range items { - if !yield(item) { - return - } - } - } else { - lc.source(func(item T) bool { - count++ - return yield(item) - }) - for count < size { - if !yield(value) { - return - } - count++ - } - } - }) -} - -// TapEach returns a new lazy collection that calls the callback on each item as it passes through. -func (lc *LazyCollection[T]) TapEach(callback func(T, int)) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - idx := 0 - lc.source(func(item T) bool { - callback(item, idx) - idx++ - return yield(item) - }) - }) -} - -// Throttle returns a new lazy collection that inserts a delay between each yielded item. -func (lc *LazyCollection[T]) Throttle(delay time.Duration) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - first := true - lc.source(func(item T) bool { - if !first { - time.Sleep(delay) - } - first = false - return yield(item) - }) - }) -} - -// Remember returns a lazy collection that caches items on first iteration, -// so subsequent iterations reuse the cached values. -func (lc *LazyCollection[T]) Remember() *LazyCollection[T] { - var cache []T - cached := false - return NewLazy(func(yield func(T) bool) { - if cached { - for _, item := range cache { - if !yield(item) { - return - } - } - return - } - cache = make([]T, 0) - lc.source(func(item T) bool { - cache = append(cache, item) - return yield(item) - }) - cached = true - }) -} - -// Every reports whether all items satisfy the callback. -func (lc *LazyCollection[T]) Every(callback func(T, int) bool) bool { - result := true - idx := 0 - lc.source(func(item T) bool { - if !callback(item, idx) { - result = false - return false - } - idx++ - return true - }) - return result -} - -// Has reports whether the given zero-based index exists in the collection. -func (lc *LazyCollection[T]) Has(index int) bool { - _, ok := lc.Get(index) - return ok -} - -// HasAny reports whether any of the given indices exist in the collection. -func (lc *LazyCollection[T]) HasAny(indices ...int) bool { - for _, idx := range indices { - if lc.Has(idx) { - return true - } - } - return false -} - -// HasSole reports whether exactly one item matches the optional predicate. -// If no predicate is given, it checks whether the collection has exactly one item. -func (lc *LazyCollection[T]) HasSole(predicates ...func(T, int) bool) bool { - count := 0 - idx := 0 - if len(predicates) == 0 || predicates[0] == nil { - lc.source(func(_ T) bool { - count++ - return count < 2 - }) - } else { - predicate := predicates[0] - lc.source(func(item T) bool { - if predicate(item, idx) { - count++ - if count > 1 { - return false - } - } - idx++ - return true - }) - } - return count == 1 -} - -// Implode joins all items into a single string separated by the given glue. -func (lc *LazyCollection[T]) Implode(glue string) string { - parts := make([]string, 0) - lc.source(func(item T) bool { - parts = append(parts, fmt.Sprint(item)) - return true - }) - return strings.Join(parts, glue) -} - -// Join joins all items into a string with the given separator. -// An optional final separator can be provided for the last element. -func (lc *LazyCollection[T]) Join(glue string, finalGlues ...string) string { - parts := make([]string, 0) - lc.source(func(item T) bool { - parts = append(parts, fmt.Sprint(item)) - return true - }) - if len(parts) == 0 { - return "" - } - if len(parts) == 1 { - return parts[0] - } - if len(finalGlues) > 0 && finalGlues[0] != "" { - last := parts[len(parts)-1] - rest := parts[:len(parts)-1] - return strings.Join(rest, glue) + finalGlues[0] + last - } - return strings.Join(parts, glue) -} - -// When applies the callback if the condition is true; otherwise applies the optional default. -func (lc *LazyCollection[T]) When(condition bool, callback func(*LazyCollection[T]) *LazyCollection[T], defaults ...func(*LazyCollection[T]) *LazyCollection[T]) *LazyCollection[T] { - if condition { - return callback(lc) - } - if len(defaults) > 0 { - return defaults[0](lc) - } - return lc -} - -// WhenEmpty applies the callback if the lazy collection is empty. -func (lc *LazyCollection[T]) WhenEmpty(callback func(*LazyCollection[T]) *LazyCollection[T], defaults ...func(*LazyCollection[T]) *LazyCollection[T]) *LazyCollection[T] { - return lc.When(lc.IsEmpty(), callback, defaults...) -} - -// WhenNotEmpty applies the callback if the lazy collection is not empty. -func (lc *LazyCollection[T]) WhenNotEmpty(callback func(*LazyCollection[T]) *LazyCollection[T], defaults ...func(*LazyCollection[T]) *LazyCollection[T]) *LazyCollection[T] { - return lc.When(lc.IsNotEmpty(), callback, defaults...) -} - -// Unless applies the callback unless the condition is true. -func (lc *LazyCollection[T]) Unless(condition bool, callback func(*LazyCollection[T]) *LazyCollection[T], defaults ...func(*LazyCollection[T]) *LazyCollection[T]) *LazyCollection[T] { - return lc.When(!condition, callback, defaults...) -} - -// LazyReduce reduces the lazy collection to a single value by applying the callback -// to an accumulator and each item in sequence. -func LazyReduce[T any, R any](lc *LazyCollection[T], callback func(R, T, int) R, initial R) R { - result := initial - idx := 0 - lc.source(func(item T) bool { - result = callback(result, item, idx) - idx++ - return true - }) - return result -} - -// LazyUnique returns a lazy collection containing only unique items as determined by the key function. -func LazyUnique[T any, K comparable](lc *LazyCollection[T], keyFunc func(T) K) *LazyCollection[T] { - return NewLazy(func(yield func(T) bool) { - seen := make(map[K]bool) - lc.source(func(item T) bool { - key := keyFunc(item) - if !seen[key] { - seen[key] = true - return yield(item) - } - return true - }) - }) -} - -// LazyPluck extracts a value from each item using the given function, returning a new lazy collection. -func LazyPluck[T any, V any](lc *LazyCollection[T], valueFunc func(T) V) *LazyCollection[V] { - return LazyMap(lc, func(item T, _ int) V { - return valueFunc(item) - }) -} - -// LazyGroupBy groups items by the key returned by the given function. -// This requires eager evaluation to build the groups. -func LazyGroupBy[T any, K comparable](lc *LazyCollection[T], keyFunc func(T) K) map[K]*LazyCollection[T] { - groups := make(map[K][]T) - lc.source(func(item T) bool { - key := keyFunc(item) - groups[key] = append(groups[key], item) - return true - }) - result := make(map[K]*LazyCollection[T]) - for key, items := range groups { - result[key] = LazyFrom(items) - } - return result -} - -// LazyKeyBy indexes items by the key returned by the given function. -// Duplicate keys cause the later value to overwrite the earlier one. -func LazyKeyBy[T any, K comparable](lc *LazyCollection[T], keyFunc func(T) K) map[K]T { - result := make(map[K]T) - lc.source(func(item T) bool { - result[keyFunc(item)] = item - return true - }) - return result -} - -// LazyCountBy counts occurrences of each key returned by the given function. -func LazyCountBy[T any, K comparable](lc *LazyCollection[T], keyFunc func(T) K) map[K]int { - result := make(map[K]int) - lc.source(func(item T) bool { - result[keyFunc(item)]++ - return true - }) - return result -} - -// Dump prints the lazy collection items to stdout for debugging and returns a new -// lazy collection backed by the evaluated items. -func (lc *LazyCollection[T]) Dump() *LazyCollection[T] { - items := lc.All() - fmt.Printf("%v\n", items) - return LazyFrom(items) -} diff --git a/collection/lazy_collection_test.go b/collection/lazy_collection_test.go deleted file mode 100644 index 3b17813..0000000 --- a/collection/lazy_collection_test.go +++ /dev/null @@ -1,465 +0,0 @@ -package collection - -import ( - "reflect" - "testing" -) - -func TestLazyFrom(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - items := lc.All() - expected := []int{1, 2, 3} - if !reflect.DeepEqual(items, expected) { - t.Errorf("expected %v, got %v", expected, items) - } -} - -func TestLazyEmpty(t *testing.T) { - lc := LazyEmpty[int]() - if !lc.IsEmpty() { - t.Error("expected empty") - } - if lc.IsNotEmpty() { - t.Error("expected empty") - } -} - -func TestLazyRange(t *testing.T) { - lc := LazyRange(1, 5) - items := lc.All() - expected := []int{1, 2, 3, 4, 5} - if !reflect.DeepEqual(items, expected) { - t.Errorf("expected %v, got %v", expected, items) - } - - lc2 := LazyRange(5, 1) - items2 := lc2.All() - expected2 := []int{5, 4, 3, 2, 1} - if !reflect.DeepEqual(items2, expected2) { - t.Errorf("expected %v, got %v", expected2, items2) - } -} - -func TestLazyTimes(t *testing.T) { - lc := LazyTimes(3, func(i int) int { return i * 10 }) - expected := []int{10, 20, 30} - if !reflect.DeepEqual(lc.All(), expected) { - t.Errorf("expected %v, got %v", expected, lc.All()) - } -} - -func TestLazyCount(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - if lc.Count() != 5 { - t.Errorf("expected 5, got %d", lc.Count()) - } -} - -func TestLazyEager(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - c := lc.Eager() - if c.Count() != 3 { - t.Errorf("expected 3, got %d", c.Count()) - } -} - -func TestLazyFirst(t *testing.T) { - lc := LazyFrom([]int{10, 20, 30}) - v, ok := lc.First() - if !ok || v != 10 { - t.Errorf("expected 10, got %d", v) - } - - v, ok = lc.First(func(item int, _ int) bool { return item > 15 }) - if !ok || v != 20 { - t.Errorf("expected 20, got %d", v) - } -} - -func TestLazyLast(t *testing.T) { - lc := LazyFrom([]int{10, 20, 30}) - v, ok := lc.Last() - if !ok || v != 30 { - t.Errorf("expected 30, got %d", v) - } - - v, ok = lc.Last(func(item int, _ int) bool { return item < 25 }) - if !ok || v != 20 { - t.Errorf("expected 20, got %d", v) - } -} - -func TestLazyGet(t *testing.T) { - lc := LazyFrom([]int{10, 20, 30}) - v, ok := lc.Get(1) - if !ok || v != 20 { - t.Errorf("expected 20, got %d", v) - } - - _, ok = lc.Get(10) - if ok { - t.Error("expected not found") - } -} - -func TestLazyContains(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - if !lc.Contains(func(item int, _ int) bool { return item == 3 }) { - t.Error("expected to contain 3") - } - if lc.Contains(func(item int, _ int) bool { return item == 10 }) { - t.Error("expected not to contain 10") - } -} - -func TestLazySearch(t *testing.T) { - lc := LazyFrom([]int{10, 20, 30}) - idx, ok := lc.Search(func(item int, _ int) bool { return item == 20 }) - if !ok || idx != 1 { - t.Errorf("expected index 1, got %d", idx) - } -} - -func TestLazyBefore(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - v, ok := lc.Before(func(item int, _ int) bool { return item == 3 }) - if !ok || v != 2 { - t.Errorf("expected 2, got %d", v) - } -} - -func TestLazyAfter(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - v, ok := lc.After(func(item int, _ int) bool { return item == 3 }) - if !ok || v != 4 { - t.Errorf("expected 4, got %d", v) - } -} - -func TestLazyFilter(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - filtered := lc.Filter(func(item int, _ int) bool { return item%2 == 0 }) - expected := []int{2, 4} - if !reflect.DeepEqual(filtered.All(), expected) { - t.Errorf("expected %v, got %v", expected, filtered.All()) - } -} - -func TestLazyReject(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - rejected := lc.Reject(func(item int, _ int) bool { return item%2 == 0 }) - expected := []int{1, 3, 5} - if !reflect.DeepEqual(rejected.All(), expected) { - t.Errorf("expected %v, got %v", expected, rejected.All()) - } -} - -func TestLazyMap(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - mapped := LazyMap(lc, func(item int, _ int) int { return item * 2 }) - expected := []int{2, 4, 6} - if !reflect.DeepEqual(mapped.All(), expected) { - t.Errorf("expected %v, got %v", expected, mapped.All()) - } -} - -func TestLazyFlatMap(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - result := LazyFlatMap(lc, func(item int, _ int) []int { return []int{item, item * 10} }) - expected := []int{1, 10, 2, 20, 3, 30} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazyTake(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - taken := lc.Take(3) - expected := []int{1, 2, 3} - if !reflect.DeepEqual(taken.All(), expected) { - t.Errorf("expected %v, got %v", expected, taken.All()) - } - - // Negative take - taken2 := lc.Take(-2) - expected2 := []int{4, 5} - if !reflect.DeepEqual(taken2.All(), expected2) { - t.Errorf("expected %v, got %v", expected2, taken2.All()) - } -} - -func TestLazyTakeUntil(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - result := lc.TakeUntil(func(item int, _ int) bool { return item == 4 }) - expected := []int{1, 2, 3} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazyTakeWhile(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - result := lc.TakeWhile(func(item int, _ int) bool { return item < 4 }) - expected := []int{1, 2, 3} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazySkip(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - result := lc.Skip(2) - expected := []int{3, 4, 5} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazySkipUntil(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - result := lc.SkipUntil(func(item int, _ int) bool { return item == 3 }) - expected := []int{3, 4, 5} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazySkipWhile(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - result := lc.SkipWhile(func(item int, _ int) bool { return item < 3 }) - expected := []int{3, 4, 5} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazySlice(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - result := lc.Slice(1, 3) - expected := []int{2, 3, 4} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazyChunk(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - chunks := lc.Chunk(2) - if len(chunks) != 3 { - t.Errorf("expected 3 chunks, got %d", len(chunks)) - } - if !reflect.DeepEqual(chunks[0], []int{1, 2}) { - t.Errorf("expected [1 2], got %v", chunks[0]) - } - if !reflect.DeepEqual(chunks[2], []int{5}) { - t.Errorf("expected [5], got %v", chunks[2]) - } -} - -func TestLazyNth(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5, 6, 7, 8}) - result := lc.Nth(3) - expected := []int{1, 4, 7} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazyConcat(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - result := lc.Concat([]int{4, 5}) - expected := []int{1, 2, 3, 4, 5} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazyPad(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - result := lc.Pad(5, 0) - expected := []int{1, 2, 3, 0, 0} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazyEvery(t *testing.T) { - lc := LazyFrom([]int{2, 4, 6, 8}) - if !lc.Every(func(item int, _ int) bool { return item%2 == 0 }) { - t.Error("expected all even") - } -} - -func TestLazyImplode(t *testing.T) { - lc := LazyFrom([]string{"a", "b", "c"}) - result := lc.Implode(", ") - if result != "a, b, c" { - t.Errorf("expected 'a, b, c', got '%s'", result) - } -} - -func TestLazyJoin(t *testing.T) { - lc := LazyFrom([]string{"a", "b", "c"}) - result := lc.Join(", ", " and ") - if result != "a, b and c" { - t.Errorf("expected 'a, b and c', got '%s'", result) - } -} - -func TestLazyReduce(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - sum := LazyReduce(lc, func(carry int, item int, _ int) int { return carry + item }, 0) - if sum != 15 { - t.Errorf("expected 15, got %d", sum) - } -} - -func TestLazyUnique(t *testing.T) { - lc := LazyFrom([]int{1, 2, 2, 3, 3, 3}) - result := LazyUnique(lc, func(item int) int { return item }) - expected := []int{1, 2, 3} - if !reflect.DeepEqual(result.All(), expected) { - t.Errorf("expected %v, got %v", expected, result.All()) - } -} - -func TestLazyPluck(t *testing.T) { - type User struct { - Name string - Age int - } - lc := LazyFrom([]User{{"Alice", 25}, {"Bob", 30}}) - names := LazyPluck(lc, func(u User) string { return u.Name }) - expected := []string{"Alice", "Bob"} - if !reflect.DeepEqual(names.All(), expected) { - t.Errorf("expected %v, got %v", expected, names.All()) - } -} - -func TestLazyGroupBy(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5, 6}) - groups := LazyGroupBy(lc, func(item int) string { - if item%2 == 0 { - return "even" - } - return "odd" - }) - if len(groups["even"].All()) != 3 { - t.Errorf("expected 3 evens, got %d", len(groups["even"].All())) - } -} - -func TestLazyKeyBy(t *testing.T) { - type User struct { - ID int - Name string - } - lc := LazyFrom([]User{{1, "Alice"}, {2, "Bob"}}) - keyed := LazyKeyBy(lc, func(u User) int { return u.ID }) - if keyed[1].Name != "Alice" { - t.Error("expected Alice") - } -} - -func TestLazyCountBy(t *testing.T) { - lc := LazyFrom([]string{"apple", "banana", "apple"}) - counts := LazyCountBy(lc, func(item string) string { return item }) - if counts["apple"] != 2 { - t.Errorf("expected 2, got %d", counts["apple"]) - } -} - -func TestLazyRemember(t *testing.T) { - callCount := 0 - lc := NewLazy(func(yield func(int) bool) { - callCount++ - for i := 1; i <= 3; i++ { - if !yield(i) { - return - } - } - }) - - remembered := lc.Remember() - // First evaluation - remembered.All() - // Second evaluation - should use cache - remembered.All() - if callCount != 1 { - t.Errorf("expected source called once, got %d", callCount) - } -} - -func TestLazyContainsOneItem(t *testing.T) { - if !LazyFrom([]int{1}).ContainsOneItem() { - t.Error("expected true") - } - if LazyFrom([]int{1, 2}).ContainsOneItem() { - t.Error("expected false") - } -} - -func TestLazyWhen(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - result := lc.When(true, func(lc *LazyCollection[int]) *LazyCollection[int] { - return lc.Take(2) - }) - if result.Count() != 2 { - t.Errorf("expected 2, got %d", result.Count()) - } -} - -func TestLazyTapEach(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - sum := 0 - result := lc.TapEach(func(item int, _ int) { - sum += item - }) - // TapEach is lazy, need to evaluate - result.All() - if sum != 6 { - t.Errorf("expected 6, got %d", sum) - } -} - -func TestCollectionToLazy(t *testing.T) { - c := New(1, 2, 3) - lc := c.Lazy() - if lc.Count() != 3 { - t.Errorf("expected 3, got %d", lc.Count()) - } -} - -func TestLazyEach(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3, 4, 5}) - sum := 0 - lc.Each(func(item int, _ int) bool { - sum += item - return item < 3 - }) - if sum != 6 { // 1+2+3 - t.Errorf("expected 6, got %d", sum) - } -} - -func TestLazySole(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - v, err := lc.Sole(func(item int, _ int) bool { return item == 2 }) - if err != nil || v != 2 { - t.Errorf("expected 2, got %d, err: %v", v, err) - } -} - -func TestLazyFirstOrFail(t *testing.T) { - lc := LazyFrom([]int{1, 2, 3}) - v, err := lc.FirstOrFail() - if err != nil || v != 1 { - t.Errorf("expected 1, got %d, err: %v", v, err) - } - - empty := LazyEmpty[int]() - _, err = empty.FirstOrFail() - if err == nil { - t.Error("expected error") - } -} diff --git a/collection/map_collection.go b/collection/map_collection.go deleted file mode 100644 index 8a188d7..0000000 --- a/collection/map_collection.go +++ /dev/null @@ -1,675 +0,0 @@ -package collection - -import ( - "encoding/json" - "fmt" - "iter" - "os" - "sort" - "strings" -) - -// MapCollection wraps a map and provides a fluent API for working with -// key-value pairs while maintaining insertion order. -type MapCollection[K comparable, V any] struct { - items map[K]V - keys []K // maintains insertion order -} - -// NewMap creates a new MapCollection from a map. -func NewMap[K comparable, V any](items map[K]V) *MapCollection[K, V] { - if items == nil { - items = make(map[K]V) - } - keys := make([]K, 0, len(items)) - for k := range items { - keys = append(keys, k) - } - return &MapCollection[K, V]{items: items, keys: keys} -} - -// NewMapFromPairs creates a MapCollection from key-value pairs. -func NewMapFromPairs[K comparable, V any](pairs ...Pair[K, V]) *MapCollection[K, V] { - items := make(map[K]V, len(pairs)) - keys := make([]K, 0, len(pairs)) - for _, p := range pairs { - if _, exists := items[p.Key]; !exists { - keys = append(keys, p.Key) - } - items[p.Key] = p.Value - } - return &MapCollection[K, V]{items: items, keys: keys} -} - -// All returns a shallow copy of the underlying map. -func (m *MapCollection[K, V]) All() map[K]V { - result := make(map[K]V, len(m.items)) - for k, v := range m.items { - result[k] = v - } - return result -} - -// Keys returns all keys as a Collection, preserving insertion order. -func (m *MapCollection[K, V]) Keys() *Collection[K] { - result := make([]K, len(m.keys)) - copy(result, m.keys) - return Collect(result) -} - -// Values returns all values as a Collection, preserving insertion order. -func (m *MapCollection[K, V]) Values() *Collection[V] { - result := make([]V, 0, len(m.keys)) - for _, k := range m.keys { - result = append(result, m.items[k]) - } - return Collect(result) -} - -// Count returns the number of items in the collection. -func (m *MapCollection[K, V]) Count() int { - return len(m.items) -} - -// IsEmpty reports whether the collection contains no items. -func (m *MapCollection[K, V]) IsEmpty() bool { - return len(m.items) == 0 -} - -// IsNotEmpty reports whether the collection contains at least one item. -func (m *MapCollection[K, V]) IsNotEmpty() bool { - return len(m.items) > 0 -} - -// Get returns the value for the given key. The second return value indicates -// whether the key was found. An optional default may be provided. -func (m *MapCollection[K, V]) Get(key K, defaults ...V) (V, bool) { - if v, ok := m.items[key]; ok { - return v, true - } - if len(defaults) > 0 { - return defaults[0], false - } - var zero V - return zero, false -} - -// GetOrPut returns the value for the given key if it exists. Otherwise it -// stores and returns the provided default value. -func (m *MapCollection[K, V]) GetOrPut(key K, value V) V { - if v, ok := m.items[key]; ok { - return v - } - m.Put(key, value) - return value -} - -// Has reports whether the given key exists in the collection. -func (m *MapCollection[K, V]) Has(key K) bool { - _, ok := m.items[key] - return ok -} - -// HasAny reports whether any of the given keys exist in the collection. -func (m *MapCollection[K, V]) HasAny(keys ...K) bool { - for _, k := range keys { - if m.Has(k) { - return true - } - } - return false -} - -// Put sets the given key-value pair in the collection. -func (m *MapCollection[K, V]) Put(key K, value V) *MapCollection[K, V] { - if _, exists := m.items[key]; !exists { - m.keys = append(m.keys, key) - } - m.items[key] = value - return m -} - -// Pull removes an item by key and returns its value. The second return value -// indicates whether the key was found. -func (m *MapCollection[K, V]) Pull(key K) (V, bool) { - v, ok := m.items[key] - if ok { - delete(m.items, key) - for i, k := range m.keys { - if k == key { - m.keys = append(m.keys[:i], m.keys[i+1:]...) - break - } - } - } - return v, ok -} - -// Forget removes one or more items by key. -func (m *MapCollection[K, V]) Forget(keys ...K) *MapCollection[K, V] { - for _, key := range keys { - m.Pull(key) - } - return m -} - -// Only returns a new collection containing only the specified keys. -func (m *MapCollection[K, V]) Only(keys ...K) *MapCollection[K, V] { - result := make(map[K]V) - newKeys := make([]K, 0, len(keys)) - for _, k := range keys { - if v, ok := m.items[k]; ok { - result[k] = v - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// Except returns a new collection containing all items except those with the -// specified keys. -func (m *MapCollection[K, V]) Except(keys ...K) *MapCollection[K, V] { - excludeSet := make(map[K]bool, len(keys)) - for _, k := range keys { - excludeSet[k] = true - } - result := make(map[K]V) - newKeys := make([]K, 0) - for _, k := range m.keys { - if !excludeSet[k] { - result[k] = m.items[k] - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// Contains reports whether any item in the collection satisfies the predicate. -func (m *MapCollection[K, V]) Contains(predicate func(V, K) bool) bool { - for _, k := range m.keys { - if predicate(m.items[k], k) { - return true - } - } - return false -} - -// Some is an alias for Contains. -func (m *MapCollection[K, V]) Some(predicate func(V, K) bool) bool { - return m.Contains(predicate) -} - -// DoesntContain reports whether no item in the collection satisfies the predicate. -func (m *MapCollection[K, V]) DoesntContain(predicate func(V, K) bool) bool { - return !m.Contains(predicate) -} - -// HasSole reports whether exactly one item in the collection satisfies the predicate. -func (m *MapCollection[K, V]) HasSole(predicate func(V, K) bool) bool { - count := 0 - for _, k := range m.keys { - if predicate(m.items[k], k) { - count++ - if count > 1 { - return false - } - } - } - return count == 1 -} - -// Search returns the key of the first item that satisfies the predicate. -// The second return value indicates whether a match was found. -func (m *MapCollection[K, V]) Search(predicate func(V, K) bool) (K, bool) { - for _, k := range m.keys { - if predicate(m.items[k], k) { - return k, true - } - } - var zero K - return zero, false -} - -// First returns the first value that satisfies the optional predicate. If no -// predicate is given, the first item in insertion order is returned. -func (m *MapCollection[K, V]) First(predicates ...func(V, K) bool) (V, bool) { - if len(m.keys) == 0 { - var zero V - return zero, false - } - if len(predicates) == 0 || predicates[0] == nil { - return m.items[m.keys[0]], true - } - predicate := predicates[0] - for _, k := range m.keys { - if predicate(m.items[k], k) { - return m.items[k], true - } - } - var zero V - return zero, false -} - -// Last returns the last value that satisfies the optional predicate. If no -// predicate is given, the last item in insertion order is returned. -func (m *MapCollection[K, V]) Last(predicates ...func(V, K) bool) (V, bool) { - if len(m.keys) == 0 { - var zero V - return zero, false - } - if len(predicates) == 0 || predicates[0] == nil { - return m.items[m.keys[len(m.keys)-1]], true - } - predicate := predicates[0] - for i := len(m.keys) - 1; i >= 0; i-- { - k := m.keys[i] - if predicate(m.items[k], k) { - return m.items[k], true - } - } - var zero V - return zero, false -} - -// Each iterates over items in insertion order, calling the callback for each -// key-value pair. Return false from the callback to stop iteration. -func (m *MapCollection[K, V]) Each(callback func(V, K) bool) *MapCollection[K, V] { - for _, k := range m.keys { - if !callback(m.items[k], k) { - break - } - } - return m -} - -// Filter returns a new collection containing only the items for which the -// callback returns true. -func (m *MapCollection[K, V]) Filter(callback func(V, K) bool) *MapCollection[K, V] { - result := make(map[K]V) - newKeys := make([]K, 0) - for _, k := range m.keys { - v := m.items[k] - if callback(v, k) { - result[k] = v - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// Reject returns a new collection containing only the items for which the -// callback returns false. -func (m *MapCollection[K, V]) Reject(callback func(V, K) bool) *MapCollection[K, V] { - return m.Filter(func(v V, k K) bool { - return !callback(v, k) - }) -} - -// MapValues transforms each value using the callback, returning a new -// MapCollection with the same keys and transformed values. -func MapValues[K comparable, V any, R any](m *MapCollection[K, V], callback func(V, K) R) *MapCollection[K, R] { - result := make(map[K]R, len(m.items)) - newKeys := make([]K, len(m.keys)) - copy(newKeys, m.keys) - for _, k := range m.keys { - result[k] = callback(m.items[k], k) - } - return &MapCollection[K, R]{items: result, keys: newKeys} -} - -// Every reports whether all items in the collection satisfy the callback. -func (m *MapCollection[K, V]) Every(callback func(V, K) bool) bool { - for _, k := range m.keys { - if !callback(m.items[k], k) { - return false - } - } - return true -} - -// Partition splits the collection into two: one where the callback returns -// true, and one where it returns false. -func (m *MapCollection[K, V]) Partition(callback func(V, K) bool) (*MapCollection[K, V], *MapCollection[K, V]) { - pass := make(map[K]V) - passKeys := make([]K, 0) - fail := make(map[K]V) - failKeys := make([]K, 0) - for _, k := range m.keys { - v := m.items[k] - if callback(v, k) { - pass[k] = v - passKeys = append(passKeys, k) - } else { - fail[k] = v - failKeys = append(failKeys, k) - } - } - return &MapCollection[K, V]{items: pass, keys: passKeys}, - &MapCollection[K, V]{items: fail, keys: failKeys} -} - -// Merge returns a new collection with the given map merged in. Existing keys -// are overwritten by the incoming values. -func (m *MapCollection[K, V]) Merge(items map[K]V) *MapCollection[K, V] { - result := make(map[K]V, len(m.items)+len(items)) - newKeys := make([]K, len(m.keys)) - copy(newKeys, m.keys) - for k, v := range m.items { - result[k] = v - } - for k, v := range items { - if _, exists := result[k]; !exists { - newKeys = append(newKeys, k) - } - result[k] = v - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// Union returns a new collection that is the union of the collection and the -// given map. Keys already present in the collection are not overwritten. -func (m *MapCollection[K, V]) Union(items map[K]V) *MapCollection[K, V] { - result := make(map[K]V, len(m.items)+len(items)) - newKeys := make([]K, len(m.keys)) - copy(newKeys, m.keys) - for k, v := range m.items { - result[k] = v - } - for k, v := range items { - if _, exists := result[k]; !exists { - result[k] = v - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// Replace returns a new collection with the given map merged in. This is an -// alias for Merge. -func (m *MapCollection[K, V]) Replace(items map[K]V) *MapCollection[K, V] { - return m.Merge(items) -} - -// MapMergeRecursive recursively merges the given map into the collection. -// When both sides have a map value for the same key, they are merged recursively. -func MapMergeRecursive[V any](m *MapCollection[string, V], items map[string]V) *MapCollection[string, V] { - result := make(map[string]V, len(m.items)+len(items)) - newKeys := make([]string, len(m.keys)) - copy(newKeys, m.keys) - for k, v := range m.items { - result[k] = v - } - for k, v := range items { - if _, exists := result[k]; !exists { - newKeys = append(newKeys, k) - } - result[k] = v - } - return &MapCollection[string, V]{items: result, keys: newKeys} -} - -// DiffKeys returns a new collection containing items whose keys are not -// present in the given map. -func (m *MapCollection[K, V]) DiffKeys(items map[K]V) *MapCollection[K, V] { - result := make(map[K]V) - newKeys := make([]K, 0) - for _, k := range m.keys { - if _, exists := items[k]; !exists { - result[k] = m.items[k] - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// DiffKeysUsing returns items whose keys are not considered equal to any key in the given map, -// using the provided comparison function. -func (m *MapCollection[K, V]) DiffKeysUsing(items map[K]V, equals func(K, K) bool) *MapCollection[K, V] { - result := make(map[K]V) - newKeys := make([]K, 0) - for _, k := range m.keys { - found := false - for otherK := range items { - if equals(k, otherK) { - found = true - break - } - } - if !found { - result[k] = m.items[k] - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// IntersectByKeys returns a new collection containing items whose keys are -// present in the given map. -func (m *MapCollection[K, V]) IntersectByKeys(items map[K]V) *MapCollection[K, V] { - result := make(map[K]V) - newKeys := make([]K, 0) - for _, k := range m.keys { - if _, exists := items[k]; exists { - result[k] = m.items[k] - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// DiffAssoc returns items whose key is not present in the given map or whose value differs. -func MapDiffAssoc[K comparable, V comparable](m *MapCollection[K, V], items map[K]V) *MapCollection[K, V] { - result := make(map[K]V) - newKeys := make([]K, 0) - for _, k := range m.keys { - otherVal, exists := items[k] - if !exists || m.items[k] != otherVal { - result[k] = m.items[k] - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// IntersectAssoc returns items whose key exists in the given map and whose value matches. -func MapIntersectAssoc[K comparable, V comparable](m *MapCollection[K, V], items map[K]V) *MapCollection[K, V] { - result := make(map[K]V) - newKeys := make([]K, 0) - for _, k := range m.keys { - if otherVal, exists := items[k]; exists && m.items[k] == otherVal { - result[k] = m.items[k] - newKeys = append(newKeys, k) - } - } - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// MapFlip swaps keys and values, returning a new MapCollection where the -// original values become keys and original keys become values. -func MapFlip[K comparable, V comparable](m *MapCollection[K, V]) *MapCollection[V, K] { - result := make(map[V]K, len(m.items)) - newKeys := make([]V, 0, len(m.keys)) - for _, k := range m.keys { - v := m.items[k] - if _, exists := result[v]; !exists { - newKeys = append(newKeys, v) - } - result[v] = k - } - return &MapCollection[V, K]{items: result, keys: newKeys} -} - -// MapSortKeys returns a new collection with string keys sorted in ascending -// order. -func MapSortKeys[V any](m *MapCollection[string, V]) *MapCollection[string, V] { - result := make(map[string]V, len(m.items)) - for k, v := range m.items { - result[k] = v - } - newKeys := make([]string, len(m.keys)) - copy(newKeys, m.keys) - sort.Strings(newKeys) - return &MapCollection[string, V]{items: result, keys: newKeys} -} - -// MapSortKeysDesc returns a new collection with string keys sorted in -// descending order. -func MapSortKeysDesc[V any](m *MapCollection[string, V]) *MapCollection[string, V] { - result := make(map[string]V, len(m.items)) - for k, v := range m.items { - result[k] = v - } - newKeys := make([]string, len(m.keys)) - copy(newKeys, m.keys) - sort.Sort(sort.Reverse(sort.StringSlice(newKeys))) - return &MapCollection[string, V]{items: result, keys: newKeys} -} - -// SortKeysUsing returns a new collection with keys sorted using the provided -// comparison function. -func (m *MapCollection[K, V]) SortKeysUsing(less func(K, K) bool) *MapCollection[K, V] { - result := make(map[K]V, len(m.items)) - for k, v := range m.items { - result[k] = v - } - newKeys := make([]K, len(m.keys)) - copy(newKeys, m.keys) - sort.SliceStable(newKeys, func(i, j int) bool { - return less(newKeys[i], newKeys[j]) - }) - return &MapCollection[K, V]{items: result, keys: newKeys} -} - -// Implode concatenates all values into a single string, separated by the -// given glue string. -func (m *MapCollection[K, V]) Implode(glue string) string { - parts := make([]string, 0, len(m.keys)) - for _, k := range m.keys { - parts = append(parts, fmt.Sprint(m.items[k])) - } - return strings.Join(parts, glue) -} - -// Join concatenates all values into a string separated by glue. An optional -// final glue is placed between the last two items. -func (m *MapCollection[K, V]) Join(glue string, finalGlues ...string) string { - parts := make([]string, 0, len(m.keys)) - for _, k := range m.keys { - parts = append(parts, fmt.Sprint(m.items[k])) - } - if len(parts) == 0 { - return "" - } - if len(parts) == 1 { - return parts[0] - } - if len(finalGlues) > 0 && finalGlues[0] != "" { - last := parts[len(parts)-1] - rest := parts[:len(parts)-1] - return strings.Join(rest, glue) + finalGlues[0] + last - } - return strings.Join(parts, glue) -} - -// Tap passes the collection to the callback and returns the collection, -// allowing side effects without breaking a method chain. -func (m *MapCollection[K, V]) Tap(callback func(*MapCollection[K, V])) *MapCollection[K, V] { - callback(m) - return m -} - -// When applies the callback if the condition is true. An optional default -// callback is applied when the condition is false. -func (m *MapCollection[K, V]) When(condition bool, callback func(*MapCollection[K, V]) *MapCollection[K, V], defaults ...func(*MapCollection[K, V]) *MapCollection[K, V]) *MapCollection[K, V] { - if condition { - return callback(m) - } - if len(defaults) > 0 { - return defaults[0](m) - } - return m -} - -// Unless applies the callback unless the condition is true. An optional -// default callback is applied when the condition is true. -func (m *MapCollection[K, V]) Unless(condition bool, callback func(*MapCollection[K, V]) *MapCollection[K, V], defaults ...func(*MapCollection[K, V]) *MapCollection[K, V]) *MapCollection[K, V] { - return m.When(!condition, callback, defaults...) -} - -// ToJSON serializes the collection to JSON. -func (m *MapCollection[K, V]) ToJSON() ([]byte, error) { - return json.Marshal(m.items) -} - -// ToPrettyJSON serializes the collection to indented JSON. -func (m *MapCollection[K, V]) ToPrettyJSON() ([]byte, error) { - return json.MarshalIndent(m.items, "", " ") -} - -// String returns the JSON representation of the collection. -func (m *MapCollection[K, V]) String() string { - b, err := m.ToJSON() - if err != nil { - return "{}" - } - return string(b) -} - -// MarshalJSON implements the json.Marshaler interface. -func (m *MapCollection[K, V]) MarshalJSON() ([]byte, error) { - return json.Marshal(m.items) -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (m *MapCollection[K, V]) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &m.items) -} - -// Copy creates a shallow copy of the collection. -func (m *MapCollection[K, V]) Copy() *MapCollection[K, V] { - return NewMap(m.All()) -} - -// Dump prints the underlying map to stdout for debugging purposes. -func (m *MapCollection[K, V]) Dump() *MapCollection[K, V] { - fmt.Printf("%v\n", m.items) - return m -} - -// DD prints the map collection for debugging and terminates the program. -func (m *MapCollection[K, V]) DD() { - m.Dump() - os.Exit(1) -} - -// ToPairs converts the collection to a Collection of Pair values, preserving -// insertion order. -func (m *MapCollection[K, V]) ToPairs() *Collection[Pair[K, V]] { - result := make([]Pair[K, V], 0, len(m.keys)) - for _, k := range m.keys { - result = append(result, Pair[K, V]{Key: k, Value: m.items[k]}) - } - return Collect(result) -} - -// ContainsOneItem reports whether the collection contains exactly one item. -func (m *MapCollection[K, V]) ContainsOneItem() bool { - return len(m.items) == 1 -} - -// ContainsManyItems reports whether the collection contains more than one item. -func (m *MapCollection[K, V]) ContainsManyItems() bool { - return len(m.items) > 1 -} - -// Iter returns an iterator over the collection's key-value pairs in insertion -// order. -func (m *MapCollection[K, V]) Iter() iter.Seq2[K, V] { - return func(yield func(K, V) bool) { - for _, k := range m.keys { - if !yield(k, m.items[k]) { - return - } - } - } -} diff --git a/collection/map_collection_test.go b/collection/map_collection_test.go deleted file mode 100644 index 30a0e5e..0000000 --- a/collection/map_collection_test.go +++ /dev/null @@ -1,440 +0,0 @@ -package collection - -import ( - "reflect" - "testing" -) - -func TestNewMap(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - if m.Count() != 3 { - t.Errorf("expected 3, got %d", m.Count()) - } -} - -func TestNewMapFromPairs(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - v, ok := m.Get("a") - if !ok || v != 1 { - t.Errorf("expected 1, got %d", v) - } -} - -func TestMapGet(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - - v, ok := m.Get("a") - if !ok || v != 1 { - t.Errorf("expected 1, got %d", v) - } - - _, ok = m.Get("z") - if ok { - t.Error("expected not found") - } - - v, _ = m.Get("z", 99) - if v != 99 { - t.Errorf("expected default 99, got %d", v) - } -} - -func TestMapGetOrPut(t *testing.T) { - m := NewMap(map[string]int{"a": 1}) - - v := m.GetOrPut("a", 99) - if v != 1 { - t.Errorf("expected existing 1, got %d", v) - } - - v = m.GetOrPut("b", 99) - if v != 99 { - t.Errorf("expected default 99, got %d", v) - } - if m.Count() != 2 { - t.Errorf("expected 2 items, got %d", m.Count()) - } -} - -func TestMapHas(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - if !m.Has("a") { - t.Error("expected to have 'a'") - } - if m.Has("z") { - t.Error("expected not to have 'z'") - } -} - -func TestMapHasAny(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - if !m.HasAny("z", "a") { - t.Error("expected HasAny to return true") - } - if m.HasAny("x", "y", "z") { - t.Error("expected HasAny to return false") - } -} - -func TestMapPut(t *testing.T) { - m := NewMap(map[string]int{}) - m.Put("a", 1) - v, ok := m.Get("a") - if !ok || v != 1 { - t.Errorf("expected 1, got %d", v) - } -} - -func TestMapPull(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - v, ok := m.Pull("a") - if !ok || v != 1 { - t.Errorf("expected 1, got %d", v) - } - if m.Has("a") { - t.Error("expected 'a' to be removed") - } -} - -func TestMapForget(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - m.Forget("a", "c") - if m.Count() != 1 { - t.Errorf("expected 1, got %d", m.Count()) - } -} - -func TestMapOnly(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - result := m.Only("a", "c") - if result.Count() != 2 { - t.Errorf("expected 2, got %d", result.Count()) - } - if !result.Has("a") || !result.Has("c") { - t.Error("expected to have 'a' and 'c'") - } -} - -func TestMapExcept(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - result := m.Except("b") - if result.Count() != 2 { - t.Errorf("expected 2, got %d", result.Count()) - } - if result.Has("b") { - t.Error("expected 'b' to be excluded") - } -} - -func TestMapKeys(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - keys := m.Keys() - if keys.Count() != 2 { - t.Errorf("expected 2 keys, got %d", keys.Count()) - } -} - -func TestMapValues(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - values := m.Values() - if values.Count() != 2 { - t.Errorf("expected 2 values, got %d", values.Count()) - } -} - -func TestMapContains(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - if !m.Contains(func(v int, k string) bool { return v == 2 }) { - t.Error("expected to contain value 2") - } - if m.Contains(func(v int, k string) bool { return v == 99 }) { - t.Error("expected not to contain 99") - } -} - -func TestMapFirst(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - v, ok := m.First() - if !ok || v != 1 { - t.Errorf("expected 1, got %d", v) - } -} - -func TestMapLast(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - v, ok := m.Last() - if !ok || v != 2 { - t.Errorf("expected 2, got %d", v) - } -} - -func TestMapFilter(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - filtered := m.Filter(func(v int, k string) bool { return v > 1 }) - if filtered.Count() != 2 { - t.Errorf("expected 2, got %d", filtered.Count()) - } -} - -func TestMapReject(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - rejected := m.Reject(func(v int, k string) bool { return v > 1 }) - if rejected.Count() != 1 { - t.Errorf("expected 1, got %d", rejected.Count()) - } -} - -func TestMapValues_Transform(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - result := MapValues(m, func(v int, k string) int { return v * 10 }) - v, _ := result.Get("a") - if v != 10 { - t.Errorf("expected 10, got %d", v) - } -} - -func TestMapEvery(t *testing.T) { - m := NewMap(map[string]int{"a": 2, "b": 4}) - if !m.Every(func(v int, k string) bool { return v%2 == 0 }) { - t.Error("expected all even") - } -} - -func TestMapPartition(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - pass, fail := m.Partition(func(v int, k string) bool { return v > 1 }) - if pass.Count() != 2 { - t.Errorf("expected 2 passing, got %d", pass.Count()) - } - if fail.Count() != 1 { - t.Errorf("expected 1 failing, got %d", fail.Count()) - } -} - -func TestMapMerge(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - result := m.Merge(map[string]int{"b": 20, "c": 3}) - v, _ := result.Get("b") - if v != 20 { - t.Errorf("expected 20 (overwritten), got %d", v) - } - if result.Count() != 3 { - t.Errorf("expected 3, got %d", result.Count()) - } -} - -func TestMapUnion(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - result := m.Union(map[string]int{"b": 20, "c": 3}) - v, _ := result.Get("b") - if v != 2 { - t.Errorf("expected 2 (not overwritten), got %d", v) - } - if result.Count() != 3 { - t.Errorf("expected 3, got %d", result.Count()) - } -} - -func TestMapDiffKeys(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - result := m.DiffKeys(map[string]int{"b": 99}) - if result.Has("b") { - t.Error("expected 'b' to be excluded") - } - if result.Count() != 2 { - t.Errorf("expected 2, got %d", result.Count()) - } -} - -func TestMapIntersectByKeys(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) - result := m.IntersectByKeys(map[string]int{"a": 99, "c": 99}) - if result.Count() != 2 { - t.Errorf("expected 2, got %d", result.Count()) - } -} - -func TestMapFlip(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - flipped := MapFlip(m) - v, ok := flipped.Get(1) - if !ok || v != "a" { - t.Errorf("expected 'a', got '%s'", v) - } -} - -func TestMapSortKeys(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "c", Value: 3}, - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - sorted := MapSortKeys(m) - keys := sorted.Keys().All() - expected := []string{"a", "b", "c"} - if !reflect.DeepEqual(keys, expected) { - t.Errorf("expected %v, got %v", expected, keys) - } -} - -func TestMapSortKeysDesc(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "c", Value: 3}, - Pair[string, int]{Key: "b", Value: 2}, - ) - sorted := MapSortKeysDesc(m) - keys := sorted.Keys().All() - expected := []string{"c", "b", "a"} - if !reflect.DeepEqual(keys, expected) { - t.Errorf("expected %v, got %v", expected, keys) - } -} - -func TestMapImplode(t *testing.T) { - m := NewMapFromPairs( - Pair[string, string]{Key: "a", Value: "hello"}, - Pair[string, string]{Key: "b", Value: "world"}, - ) - result := m.Implode(", ") - if result != "hello, world" { - t.Errorf("expected 'hello, world', got '%s'", result) - } -} - -func TestMapToJSON(t *testing.T) { - m := NewMap(map[string]int{"a": 1}) - b, err := m.ToJSON() - if err != nil { - t.Fatal(err) - } - if string(b) != `{"a":1}` { - t.Errorf("expected {\"a\":1}, got %s", string(b)) - } -} - -func TestMapCopy(t *testing.T) { - m := NewMap(map[string]int{"a": 1}) - m2 := m.Copy() - m.Put("b", 2) - if m2.Has("b") { - t.Error("copy should not be affected") - } -} - -func TestMapToPairs(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - pairs := m.ToPairs() - if pairs.Count() != 2 { - t.Errorf("expected 2 pairs, got %d", pairs.Count()) - } -} - -func TestMapWhen(t *testing.T) { - m := NewMap(map[string]int{"a": 1}) - result := m.When(true, func(mc *MapCollection[string, int]) *MapCollection[string, int] { - mc.Put("b", 2) - return mc - }) - if !result.Has("b") { - t.Error("expected 'b' to be added") - } -} - -func TestMapSearch(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - key, ok := m.Search(func(v int, k string) bool { return v == 2 }) - if !ok || key != "b" { - t.Errorf("expected 'b', got '%s'", key) - } -} - -func TestMapIsEmpty(t *testing.T) { - m := NewMap(map[string]int{}) - if !m.IsEmpty() { - t.Error("expected empty") - } - if m.IsNotEmpty() { - t.Error("expected empty") - } -} - -func TestMapContainsOneItem(t *testing.T) { - m := NewMap(map[string]int{"a": 1}) - if !m.ContainsOneItem() { - t.Error("expected true") - } -} - -func TestMapContainsManyItems(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - if !m.ContainsManyItems() { - t.Error("expected true") - } -} - -func TestMapEach(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "a", Value: 1}, - Pair[string, int]{Key: "b", Value: 2}, - ) - sum := 0 - m.Each(func(v int, k string) bool { - sum += v - return true - }) - if sum != 3 { - t.Errorf("expected 3, got %d", sum) - } -} - -func TestMapDoesntContain(t *testing.T) { - m := NewMap(map[string]int{"a": 1}) - if !m.DoesntContain(func(v int, k string) bool { return v == 99 }) { - t.Error("expected true") - } -} - -func TestMapSortKeysUsing(t *testing.T) { - m := NewMapFromPairs( - Pair[string, int]{Key: "banana", Value: 1}, - Pair[string, int]{Key: "apple", Value: 2}, - Pair[string, int]{Key: "cherry", Value: 3}, - ) - sorted := m.SortKeysUsing(func(a, b string) bool { return a < b }) - keys := sorted.Keys().All() - expected := []string{"apple", "banana", "cherry"} - if !reflect.DeepEqual(keys, expected) { - t.Errorf("expected %v, got %v", expected, keys) - } -} - -func TestMapReplace(t *testing.T) { - m := NewMap(map[string]int{"a": 1, "b": 2}) - result := m.Replace(map[string]int{"b": 20, "c": 3}) - v, _ := result.Get("b") - if v != 20 { - t.Errorf("expected 20, got %d", v) - } -} diff --git a/collection/mutation.go b/collection/mutation.go new file mode 100644 index 0000000..5c742a4 --- /dev/null +++ b/collection/mutation.go @@ -0,0 +1,150 @@ +package collection + +import "fmt" + +// Put sets the item at the given index to the given value. +func (c *Collection[T]) Put(index int, value T) *Collection[T] { + if index >= 0 && index < len(c.items) { + c.items[index] = value + } + + return c +} + +// Pull removes and returns the item at the given index. +// The second return value indicates whether the index was valid. +func (c *Collection[T]) Pull(index int) (T, bool) { + if index < 0 || index >= len(c.items) { + var zero T + + return zero, false + } + + item := c.items[index] + c.items = append(c.items[:index], c.items[index+1:]...) + + return item, true +} + +// Push appends one or more items to the end of the collection. +func (c *Collection[T]) Push(values ...T) *Collection[T] { + c.items = append(c.items, values...) + + return c +} + +// Add appends a single item to the end of the collection. +func (c *Collection[T]) Add(item T) *Collection[T] { + return c.Push(item) +} + +// Prepend adds an item to the beginning of the collection. +func (c *Collection[T]) Prepend(value T) *Collection[T] { + c.items = append([]T{value}, c.items...) + + return c +} + +// Unshift is an alias for Prepend. +func (c *Collection[T]) Unshift(value T) *Collection[T] { + return c.Prepend(value) +} + +// Pop removes and returns the last item from the collection. +// The second return value indicates whether the collection was non-empty. +func (c *Collection[T]) Pop(counts ...int) (T, bool) { + if len(c.items) == 0 { + var zero T + + return zero, false + } + + count := 1 + + if len(counts) > 0 { + count = counts[0] + } + + _ = count // For single pop + item := c.items[len(c.items)-1] + c.items = c.items[:len(c.items)-1] + + return item, true +} + +// PopMany removes and returns the last n items from the collection. +func (c *Collection[T]) PopMany(count int) *Collection[T] { + if count >= len(c.items) { + popped := Collect(c.items) + c.items = make([]T, 0) + + return popped + } + + idx := len(c.items) - count + popped := Collect(c.items[idx:]) + c.items = c.items[:idx] + + return popped +} + +// Shift removes and returns the first item from the collection. +// The second return value indicates whether the collection was non-empty. +func (c *Collection[T]) Shift() (T, bool) { + if len(c.items) == 0 { + var zero T + + return zero, false + } + + item := c.items[0] + c.items = c.items[1:] + + return item, true +} + +// ShiftMany removes and returns the first n items from the collection. +func (c *Collection[T]) ShiftMany(count int) *Collection[T] { + if count >= len(c.items) { + shifted := Collect(c.items) + c.items = make([]T, 0) + + return shifted + } + + shifted := Collect(c.items[:count]) + c.items = c.items[count:] + + return shifted +} + +// Forget removes an item from the collection by index, mutating the collection. +func (c *Collection[T]) Forget(index int) *Collection[T] { + if index < 0 || index >= len(c.items) { + return c + } + + c.items = append(c.items[:index], c.items[index+1:]...) + + return c +} + +// Transform applies the callback to each item in place, mutating the collection. +func (c *Collection[T]) Transform(callback func(T, int) T) *Collection[T] { + for i, item := range c.items { + c.items[i] = callback(item, i) + } + + return c +} + +// Ensure verifies that all items satisfy the given predicate, returning an error if any do not. +func (c *Collection[T]) Ensure(predicate func(T) bool) error { + for _, item := range c.items { + if !predicate(item) { + return fmt.Errorf("collection item failed ensure check") + } + } + + return nil +} diff --git a/collection/partition.go b/collection/partition.go new file mode 100644 index 0000000..4e7ce07 --- /dev/null +++ b/collection/partition.go @@ -0,0 +1,359 @@ +package collection + +import "math" + +// Chunk breaks the collection into multiple slices of the given size. +func (c *Collection[T]) Chunk(size int) [][]T { + if size <= 0 { + return nil + } + + chunks := make([][]T, 0) + + for i := 0; i < len(c.items); i += size { + end := i + size + + if end > len(c.items) { + end = len(c.items) + } + + chunk := make([]T, end-i) + copy(chunk, c.items[i:end]) + chunks = append(chunks, chunk) + } + + return chunks +} + +// ChunkWhile breaks the collection into groups as long as the callback returns true. +// A new group is started each time the callback returns false. +func (c *Collection[T]) ChunkWhile(callback func(T, int, []T) bool) [][]T { + if len(c.items) == 0 { + return nil + } + + chunks := make([][]T, 0) + current := []T{c.items[0]} + + for i := 1; i < len(c.items); i++ { + if callback(c.items[i], i, current) { + current = append(current, c.items[i]) + } else { + chunks = append(chunks, current) + current = []T{c.items[i]} + } + } + + chunks = append(chunks, current) + + return chunks +} + +// Split breaks the collection into the given number of groups. +func (c *Collection[T]) Split(numberOfGroups int) [][]T { + if len(c.items) == 0 || numberOfGroups <= 0 { + return nil + } + + groups := make([][]T, 0, numberOfGroups) + groupSize := float64(len(c.items)) / float64(numberOfGroups) + + for i := 0; i < numberOfGroups; i++ { + start := int(math.Round(float64(i) * groupSize)) + end := int(math.Round(float64(i+1) * groupSize)) + + if start >= len(c.items) { + break + } + + if end > len(c.items) { + end = len(c.items) + } + + chunk := make([]T, end-start) + copy(chunk, c.items[start:end]) + groups = append(groups, chunk) + } + + return groups +} + +// SplitIn splits the collection into groups, filling non-terminal groups. +func (c *Collection[T]) SplitIn(numberOfGroups int) [][]T { + size := int(math.Ceil(float64(len(c.items)) / float64(numberOfGroups))) + + return c.Chunk(size) +} + +// Sliding returns a sliding window view of the collection with the given window size and step. +func (c *Collection[T]) Sliding(size int, steps ...int) [][]T { + step := 1 + + if len(steps) > 0 { + step = steps[0] + } + + if size <= 0 || step <= 0 || len(c.items) == 0 { + return nil + } + + chunks := make([][]T, 0) + + for i := 0; i+size <= len(c.items); i += step { + chunk := make([]T, size) + copy(chunk, c.items[i:i+size]) + chunks = append(chunks, chunk) + } + + return chunks +} + +// Slice extracts a portion of the collection starting at the given offset. +// An optional length limits how many items are returned. +func (c *Collection[T]) Slice(offset int, lengths ...int) *Collection[T] { + if offset < 0 { + offset = len(c.items) + offset + + if offset < 0 { + offset = 0 + } + } + + if offset >= len(c.items) { + return Empty[T]() + } + + if len(lengths) > 0 { + end := offset + lengths[0] + + if end > len(c.items) { + end = len(c.items) + } + + result := make([]T, end-offset) + copy(result, c.items[offset:end]) + + return Collect(result) + } + + result := make([]T, len(c.items)-offset) + copy(result, c.items[offset:]) + + return Collect(result) +} + +// Splice removes and returns a slice of items starting at the given offset. +// An optional length limits the number of items removed. +func (c *Collection[T]) Splice(offset int, lengths ...int) *Collection[T] { + length := len(c.items) - offset + + if len(lengths) > 0 { + length = lengths[0] + } + + if offset < 0 { + offset = len(c.items) + offset + + if offset < 0 { + offset = 0 + } + } + + if offset >= len(c.items) { + return Empty[T]() + } + + end := offset + length + + if end > len(c.items) { + end = len(c.items) + } + + removed := make([]T, end-offset) + copy(removed, c.items[offset:end]) + c.items = append(c.items[:offset], c.items[end:]...) + + return Collect(removed) +} + +// SpliceReplace removes a portion at the given offset and replaces it with the provided items. +// It returns the removed items. +func (c *Collection[T]) SpliceReplace(offset, length int, replacement []T) *Collection[T] { + if offset < 0 { + offset = len(c.items) + offset + + if offset < 0 { + offset = 0 + } + } + + end := offset + length + + if end > len(c.items) { + end = len(c.items) + } + + removed := make([]T, end-offset) + copy(removed, c.items[offset:end]) + newItems := make([]T, 0, len(c.items)-length+len(replacement)) + newItems = append(newItems, c.items[:offset]...) + newItems = append(newItems, replacement...) + newItems = append(newItems, c.items[end:]...) + c.items = newItems + + return Collect(removed) +} + +// Take returns a new collection with the specified number of items from the front. +// A negative limit takes from the end. +func (c *Collection[T]) Take(limit int) *Collection[T] { + if limit < 0 { + return c.Slice(limit) + } + + return c.Slice(0, limit) +} + +// TakeUntil returns items from the start until the callback returns true. +func (c *Collection[T]) TakeUntil(callback func(T, int) bool) *Collection[T] { + result := make([]T, 0) + + for i, item := range c.items { + if callback(item, i) { + break + } + + result = append(result, item) + } + + return Collect(result) +} + +// TakeWhile returns items from the start as long as the callback returns true. +func (c *Collection[T]) TakeWhile(callback func(T, int) bool) *Collection[T] { + return c.TakeUntil(func(item T, index int) bool { + return !callback(item, index) + }) +} + +// Skip returns a new collection with the first n items removed. +func (c *Collection[T]) Skip(count int) *Collection[T] { + return c.Slice(count) +} + +// SkipUntil skips items until the callback returns true, then returns the rest. +func (c *Collection[T]) SkipUntil(callback func(T, int) bool) *Collection[T] { + result := make([]T, 0) + found := false + + for i, item := range c.items { + if !found && callback(item, i) { + found = true + } + + if found { + result = append(result, item) + } + } + + return Collect(result) +} + +// SkipWhile skips items as long as the callback returns true, then returns the rest. +func (c *Collection[T]) SkipWhile(callback func(T, int) bool) *Collection[T] { + return c.SkipUntil(func(item T, index int) bool { + return !callback(item, index) + }) +} + +// Nth returns a new collection containing every n-th element, starting at an optional offset. +func (c *Collection[T]) Nth(step int, offsets ...int) *Collection[T] { + offset := 0 + + if len(offsets) > 0 { + offset = offsets[0] + } + + result := make([]T, 0) + + for i := offset; i < len(c.items); i += step { + result = append(result, c.items[i]) + } + + return Collect(result) +} + +// ForPage returns a subset of items for the given page number and page size. +func (c *Collection[T]) ForPage(page, perPage int) *Collection[T] { + offset := (page - 1) * perPage + + return c.Slice(offset, perPage) +} + +// Partition splits the collection into two: items that pass the predicate and items that do not. +func (c *Collection[T]) Partition(callback func(T, int) bool) (*Collection[T], *Collection[T]) { + pass := make([]T, 0) + fail := make([]T, 0) + + for i, item := range c.items { + if callback(item, i) { + pass = append(pass, item) + } else { + fail = append(fail, item) + } + } + + return Collect(pass), Collect(fail) +} + +// Concat returns a new collection with the given items appended. +func (c *Collection[T]) Concat(items []T) *Collection[T] { + result := make([]T, len(c.items)+len(items)) + copy(result, c.items) + copy(result[len(c.items):], items) + + return Collect(result) +} + +// Merge returns a new collection with the given items merged in (appended). +func (c *Collection[T]) Merge(items []T) *Collection[T] { + return c.Concat(items) +} + +// Pad pads the collection to the specified length with the given value. +// A negative size pad on the left; a positive size pad on the right. +func (c *Collection[T]) Pad(size int, value T) *Collection[T] { + absSize := size + + if absSize < 0 { + absSize = -absSize + } + + if len(c.items) >= absSize { + result := make([]T, len(c.items)) + copy(result, c.items) + + return Collect(result) + } + + padCount := absSize - len(c.items) + padding := make([]T, padCount) + + for i := range padding { + padding[i] = value + } + + if size < 0 { + result := make([]T, absSize) + copy(result, padding) + copy(result[padCount:], c.items) + + return Collect(result) + } + + result := make([]T, absSize) + copy(result, c.items) + copy(result[len(c.items):], padding) + + return Collect(result) +} diff --git a/collection/pipeline.go b/collection/pipeline.go new file mode 100644 index 0000000..48baeb7 --- /dev/null +++ b/collection/pipeline.go @@ -0,0 +1,186 @@ +package collection + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/gocanto/collection/arr" +) + +// Each iterates over the items, passing each item and its index to the callback. +// Return false from the callback to stop iterating. +func (c *Collection[T]) Each(callback func(T, int) bool) *Collection[T] { + for i, item := range c.items { + if !callback(item, i) { + break + } + } + + return c +} + +// EachSpread iterates over the collection's items, passing each item value +// into the given callback. In Go this operates the same as Each. +func (c *Collection[T]) EachSpread(callback func(T, int) bool) *Collection[T] { + return c.Each(callback) +} + +// Tap passes the collection to the given callback and returns the collection unchanged. +func (c *Collection[T]) Tap(callback func(*Collection[T])) *Collection[T] { + callback(c) + + return c +} + +// TapEach calls the given callback on each item for side effects, returning the original collection. +func (c *Collection[T]) TapEach(callback func(T, int)) *Collection[T] { + for i, item := range c.items { + callback(item, i) + } + + return c +} + +// Pipe passes the collection to the given callback and returns the callback's result. +func Pipe[T any, R any](c *Collection[T], callback func(*Collection[T]) R) R { + return callback(c) +} + +// PipeInto passes the collection to the given constructor and returns the result. +func PipeInto[T any, R any](c *Collection[T], constructor func(*Collection[T]) R) R { + return constructor(c) +} + +// PipeThrough passes the collection through a series of callbacks, returning the final result. +func PipeThrough[T any](c *Collection[T], callbacks ...func(*Collection[T]) *Collection[T]) *Collection[T] { + result := c + + for _, cb := range callbacks { + result = cb(result) + } + + return result +} + +// When applies the callback if the condition is true; otherwise it applies the optional default callback. +func (c *Collection[T]) When(condition bool, callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + if condition { + return callback(c) + } + + if len(defaults) > 0 { + return defaults[0](c) + } + + return c +} + +// WhenEmpty applies the callback when the collection is empty. +func (c *Collection[T]) WhenEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + return c.When(c.IsEmpty(), callback, defaults...) +} + +// WhenNotEmpty applies the callback when the collection is not empty. +func (c *Collection[T]) WhenNotEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + return c.When(c.IsNotEmpty(), callback, defaults...) +} + +// Unless applies the callback unless the condition is true. +func (c *Collection[T]) Unless(condition bool, callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + return c.When(!condition, callback, defaults...) +} + +// UnlessEmpty applies the callback unless the collection is empty. +func (c *Collection[T]) UnlessEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + return c.WhenNotEmpty(callback, defaults...) +} + +// UnlessNotEmpty applies the callback unless the collection is not empty. +func (c *Collection[T]) UnlessNotEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + return c.WhenEmpty(callback, defaults...) +} + +// Implode joins elements into a string using the given glue, converting each item via fmt.Sprint. +func (c *Collection[T]) Implode(glue string) string { + parts := make([]string, len(c.items)) + + for i, item := range c.items { + parts[i] = fmt.Sprint(item) + } + + return strings.Join(parts, glue) +} + +// Join joins elements into a string using the given glue, +// with an optional final separator between the last two items. +func (c *Collection[T]) Join(glue string, finalGlues ...string) string { + parts := make([]string, len(c.items)) + + for i, item := range c.items { + parts[i] = fmt.Sprint(item) + } + + return arr.Join(parts, glue, finalGlues...) +} + +// ToSlice returns a copy of the underlying slice. +func (c *Collection[T]) ToSlice() []T { + result := make([]T, len(c.items)) + copy(result, c.items) + + return result +} + +// ToJSON serializes the collection to JSON bytes. +func (c *Collection[T]) ToJSON() ([]byte, error) { + return json.Marshal(c.items) +} + +// ToPrettyJSON serializes the collection to indented JSON bytes. +func (c *Collection[T]) ToPrettyJSON() ([]byte, error) { + return json.MarshalIndent(c.items, "", " ") +} + +// String returns the JSON string representation of the collection. +func (c *Collection[T]) String() string { + b, err := c.ToJSON() + + if err != nil { + return "[]" + } + + return string(b) +} + +// MarshalJSON implements the json.Marshaler interface. +func (c *Collection[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(c.items) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *Collection[T]) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &c.items) +} + +// Copy creates a shallow copy of the collection. +func (c *Collection[T]) Copy() *Collection[T] { + result := make([]T, len(c.items)) + copy(result, c.items) + + return Collect(result) +} + +// Dump prints the collection items to stdout for debugging. +func (c *Collection[T]) Dump() *Collection[T] { + fmt.Printf("%v\n", c.items) + + return c +} + +// DD prints the collection items for debugging and terminates the program. +func (c *Collection[T]) DD() { + c.Dump() + os.Exit(1) +} diff --git a/collection/sorting.go b/collection/sorting.go new file mode 100644 index 0000000..54c0a6d --- /dev/null +++ b/collection/sorting.go @@ -0,0 +1,79 @@ +package collection + +import ( + "cmp" + "math/rand/v2" + "sort" +) + +// Sort returns a new collection sorted using the provided comparison function. +func (c *Collection[T]) Sort(less func(a, b T) bool) *Collection[T] { + result := make([]T, len(c.items)) + copy(result, c.items) + + sort.SliceStable(result, func(i, j int) bool { + return less(result[i], result[j]) + }) + + return Collect(result) +} + +// SortBy returns a new collection sorted in ascending order by the given key function. +func SortBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) *Collection[T] { + result := make([]T, len(c.items)) + copy(result, c.items) + + sort.SliceStable(result, func(i, j int) bool { + return keyFunc(result[i]) < keyFunc(result[j]) + }) + + return Collect(result) +} + +// SortByDesc returns a new collection sorted in descending order by the given key function. +func SortByDesc[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) *Collection[T] { + result := make([]T, len(c.items)) + copy(result, c.items) + + sort.SliceStable(result, func(i, j int) bool { + return keyFunc(result[i]) > keyFunc(result[j]) + }) + + return Collect(result) +} + +// SortDesc returns a new collection sorted in descending order using the provided comparison function. +func (c *Collection[T]) SortDesc(less func(a, b T) bool) *Collection[T] { + return c.Sort(func(a, b T) bool { + return less(b, a) + }) +} + +// Shuffle returns a new collection with items in random order. +func (c *Collection[T]) Shuffle() *Collection[T] { + result := make([]T, len(c.items)) + copy(result, c.items) + + rand.Shuffle(len(result), func(i, j int) { + result[i], result[j] = result[j], result[i] + }) + + return Collect(result) +} + +// Random returns a new collection with the specified number of randomly selected items. +func (c *Collection[T]) Random(counts ...int) *Collection[T] { + count := 1 + + if len(counts) > 0 { + count = counts[0] + } + + shuffled := c.Shuffle() + + if count >= len(shuffled.items) { + return shuffled + } + + return Collect(shuffled.items[:count]) +} diff --git a/collection/transform.go b/collection/transform.go new file mode 100644 index 0000000..4a91c36 --- /dev/null +++ b/collection/transform.go @@ -0,0 +1,299 @@ +package collection + +import "cmp" + +// Filter returns a new collection containing only items for which the callback returns true. +func (c *Collection[T]) Filter(callback func(T, int) bool) *Collection[T] { + result := make([]T, 0) + + for i, item := range c.items { + if callback(item, i) { + result = append(result, item) + } + } + + return Collect(result) +} + +// Reject returns a new collection containing only items for which the callback returns false. +func (c *Collection[T]) Reject(callback func(T, int) bool) *Collection[T] { + return c.Filter(func(item T, index int) bool { + return !callback(item, index) + }) +} + +// Map applies the callback to each item and returns a new collection of results. +func Map[T any, R any](c *Collection[T], callback func(T, int) R) *Collection[R] { + result := make([]R, len(c.items)) + + for i, item := range c.items { + result[i] = callback(item, i) + } + + return Collect(result) +} + +// FlatMap applies the callback to each item, flattening the resulting slices into a single collection. +func FlatMap[T any, R any](c *Collection[T], callback func(T, int) []R) *Collection[R] { + result := make([]R, 0) + + for i, item := range c.items { + result = append(result, callback(item, i)...) + } + + return Collect(result) +} + +// MapInto applies the constructor to each item, returning a new collection of the mapped types. +func MapInto[T any, R any](c *Collection[T], constructor func(T) R) *Collection[R] { + result := make([]R, len(c.items)) + + for i, item := range c.items { + result[i] = constructor(item) + } + + return Collect(result) +} + +// Reduce iterating over the collection and accumulates a single result using the callback. +func Reduce[T any, R any](c *Collection[T], callback func(R, T, int) R, initial R) R { + result := initial + + for i, item := range c.items { + result = callback(result, item, i) + } + + return result +} + +// Flatten returns a shallow copy of the collection. +// For non-nested typed slices this returns the items as-is. +func (c *Collection[T]) Flatten() *Collection[T] { + return Collect(append([]T{}, c.items...)) +} + +// Reverse returns a new collection with items in reverse order. +func (c *Collection[T]) Reverse() *Collection[T] { + result := make([]T, len(c.items)) + + for i, j := 0, len(c.items)-1; j >= 0; i, j = i+1, j-1 { + result[i] = c.items[j] + } + + return Collect(result) +} + +// Flip returns a new collection with the item order reversed. +// For typed Go slices this reverses the element order. +func (c *Collection[T]) Flip() *Collection[T] { + result := make([]T, len(c.items)) + + for i, j := 0, len(c.items)-1; j >= 0; i, j = i+1, j-1 { + result[i] = c.items[j] + } + + return Collect(result) +} + +// Multiply returns a new collection with all items repeated the given number of times. +func (c *Collection[T]) Multiply(multiplier int) *Collection[T] { + if multiplier <= 0 { + return Empty[T]() + } + + result := make([]T, 0, len(c.items)*multiplier) + + for i := 0; i < multiplier; i++ { + result = append(result, c.items...) + } + + return Collect(result) +} + +// Values returns a new collection with re-indexed items (a shallow copy). +func (c *Collection[T]) Values() *Collection[T] { + result := make([]T, len(c.items)) + copy(result, c.items) + + return Collect(result) +} + +// Keys returns a new Collection[int] containing the indices 0 through n-1. +func (c *Collection[T]) Keys() *Collection[int] { + keys := make([]int, len(c.items)) + + for i := range c.items { + keys[i] = i + } + + return Collect(keys) +} + +// Unique returns a new collection containing only items with distinct keys +// as determined by the given key function. +func Unique[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] { + seen := make(map[K]bool) + result := make([]T, 0) + + for _, item := range c.items { + key := keyFunc(item) + + if !seen[key] { + seen[key] = true + result = append(result, item) + } + } + + return Collect(result) +} + +// Duplicates returns a new collection containing all duplicate items +// as determined by the given key function. +func Duplicates[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] { + seen := make(map[K]bool) + result := make([]T, 0) + + for _, item := range c.items { + key := keyFunc(item) + + if seen[key] { + result = append(result, item) + } else { + seen[key] = true + } + } + + return Collect(result) +} + +// Pluck extracts a value from each item using the given function, returning a new collection. +func Pluck[T any, V any](c *Collection[T], valueFunc func(T) V) *Collection[V] { + result := make([]V, len(c.items)) + + for i, item := range c.items { + result[i] = valueFunc(item) + } + + return Collect(result) +} + +// Every report whether all items in the collection satisfy the given predicate. +func (c *Collection[T]) Every(callback func(T, int) bool) bool { + for i, item := range c.items { + if !callback(item, i) { + return false + } + } + + return true +} + +// Where filters items using a predicate that receives only the item (no index). +func (c *Collection[T]) Where(predicate func(T) bool) *Collection[T] { + return c.Filter(func(item T, _ int) bool { + return predicate(item) + }) +} + +// WhereNot filters items using a negative predicate that receives only the item (no index). +func (c *Collection[T]) WhereNot(predicate func(T) bool) *Collection[T] { + return c.Filter(func(item T, _ int) bool { + return !predicate(item) + }) +} + +// WhereNull returns items whose extracted value equals the zero value. +func WhereNull[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] { + var zero K + result := make([]T, 0) + + for _, item := range c.items { + if keyFunc(item) == zero { + result = append(result, item) + } + } + + return Collect(result) +} + +// WhereNotNull returns items whose extracted value is not the zero value. +func WhereNotNull[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] { + var zero K + result := make([]T, 0) + + for _, item := range c.items { + if keyFunc(item) != zero { + result = append(result, item) + } + } + + return Collect(result) +} + +// WhereIn returns items whose extracted key value is in the given set. +func WhereIn[T any, K comparable](c *Collection[T], keyFunc func(T) K, values []K) *Collection[T] { + set := make(map[K]bool, len(values)) + + for _, v := range values { + set[v] = true + } + + result := make([]T, 0) + + for _, item := range c.items { + if set[keyFunc(item)] { + result = append(result, item) + } + } + + return Collect(result) +} + +// WhereNotIn returns items whose extracted key value is not in the given set. +func WhereNotIn[T any, K comparable](c *Collection[T], keyFunc func(T) K, values []K) *Collection[T] { + set := make(map[K]bool, len(values)) + + for _, v := range values { + set[v] = true + } + + result := make([]T, 0) + + for _, item := range c.items { + if !set[keyFunc(item)] { + result = append(result, item) + } + } + + return Collect(result) +} + +// WhereBetween returns items whose extracted key value is between min and max (inclusive). +func WhereBetween[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K, min, max K) *Collection[T] { + result := make([]T, 0) + + for _, item := range c.items { + v := keyFunc(item) + + if v >= min && v <= max { + result = append(result, item) + } + } + + return Collect(result) +} + +// WhereNotBetween returns items whose extracted key value is outside the range [min, max]. +func WhereNotBetween[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K, min, max K) *Collection[T] { + result := make([]T, 0) + + for _, item := range c.items { + v := keyFunc(item) + + if v < min || v > max { + result = append(result, item) + } + } + + return Collect(result) +} diff --git a/docs/arr.md b/docs/arr.md index 565b7d3..198ad03 100644 --- a/docs/arr.md +++ b/docs/arr.md @@ -1,8 +1,6 @@ -# arr -- Generic Slice Utilities +# 🧩 Array Utilities (`arr`) -``` -import "github.com/gocanto/collection/arr" -``` +`import "github.com/gocanto/collection/arr"` The `arr` package provides standalone, generic helper functions for working with Go slices. Every function operates on plain slices, returns new slices (the original is never mutated), and can be called without constructing a collection object. @@ -10,47 +8,49 @@ Use `arr` when you need a single operation on a slice and do not need the fluent --- -## Table of Contents - -- [Accessible](#accessible) -- [IsList](#islist) -- [First](#first) -- [Last](#last) -- [Take](#take) -- [Only](#only) -- [Except](#except) -- [Flatten](#flatten) -- [Collapse](#collapse) -- [Wrap](#wrap) -- [WrapSlice](#wrapslice) -- [Prepend](#prepend) -- [Push](#push) -- [Shuffle](#shuffle) -- [Random](#random) -- [Sort](#sort) -- [SortDesc](#sortdesc) -- [SortRecursive](#sortrecursive) -- [Where](#where) -- [WhereNotNull](#wherenotnull) -- [Reject](#reject) -- [Partition](#partition) -- [Every](#every) -- [Some](#some) -- [Exists](#exists) -- [Has](#has) -- [HasAny](#hasany) -- [Join](#join) -- [CrossJoin](#crossjoin) -- [Divide](#divide) -- [Map](#map) -- [MapWithKeys](#mapwithkeys) -- [MapSpread](#mapspread) -- [KeyBy](#keyby) -- [Pluck](#pluck) - ---- - -## Accessible +## πŸ›  Available Functions + +| Function | Purpose | +|:---|:---| +| [**Accessible**](#accessible) | Reports whether the given value is non-nil. | +| [**IsList**](#islist) | Reports whether the given slice is a sequential list. | +| [**First**](#first) | Returns the first element or the first matching a predicate. | +| [**Last**](#last) | Returns the last element or the last matching a predicate. | +| [**Take**](#take) | Returns a new slice containing up to N elements. | +| [**Only**](#only) | Returns a new slice containing only the elements at the given indices. | +| [**Except**](#except) | Returns a new slice containing all elements except those at the given indices. | +| [**Flatten**](#flatten) | Flattens a slice of slices into a single, flat slice. | +| [**Collapse**](#collapse) | Alias for `Flatten`. | +| [**Wrap**](#wrap) | Wraps a single value in a one-element slice. | +| [**WrapSlice**](#wrapslice) | Returns the given slice unchanged. | +| [**Prepend**](#prepend) | Inserts a value at the beginning of the slice. | +| [**Push**](#push) | Appends one or more values to the end of the slice. | +| [**Shuffle**](#shuffle) | Returns a new slice with the elements in random order. | +| [**Random**](#random) | Returns a new slice containing N randomly selected elements. | +| [**Sort**](#sort) | Returns a new slice sorted using a comparison function. | +| [**SortDesc**](#sortdesc) | Returns a new slice sorted in descending order. | +| [**SortRecursive**](#sortrecursive) | Sorts a slice using a comparison function. | +| [**Where**](#where) | Returns a new slice containing only elements matching a callback. | +| [**WhereNotNull**](#wherenotnull) | Returns a new slice with all zero-value elements removed. | +| [**Reject**](#reject) | Returns a new slice excluding elements matching a callback. | +| [**Partition**](#partition) | Splits the slice into two based on a callback. | +| [**Every**](#every) | Reports whether every element satisfies a callback. | +| [**Some**](#some) | Reports whether at least one element satisfies a callback. | +| [**Exists**](#exists) | Reports whether the given index is valid. | +| [**Has**](#has) | Reports whether all of the given indices are valid. | +| [**HasAny**](#hasany) | Reports whether at least one of the given indices is valid. | +| [**Join**](#join) | Concatenates string slice elements with a glue string. | +| [**CrossJoin**](#crossjoin) | Returns the Cartesian product of the given slices. | +| [**Divide**](#divide) | Splits a slice into indices and values. | +| [**Map**](#map) | Transforms each element into a new slice of results. | +| [**MapWithKeys**](#mapwithkeys) | Transforms each element into a key-value pair map. | +| [**MapSpread**](#mapspread) | Alias for `Map`. | +| [**KeyBy**](#keyby) | Indexes the slice elements by a key returned from a function. | +| [**Pluck**](#pluck) | Extracts a single field from each element. | + +--- + +## πŸ’Ž Accessible ```go func Accessible(value any) bool @@ -67,34 +67,30 @@ m := map[string]int{} arr.Accessible(m) // true ``` -**Why it is useful:** Provides a clean, intention-revealing nil check when working with `any`-typed values received from configuration maps, JSON decoding, or other untyped sources. - --- -## IsList +## πŸ’Ž IsList ```go func IsList[T any](items []T) bool ``` -Reports whether the given slice is a sequential list. In Go every slice is inherently a sequential, integer-indexed list, so this always returns `true`. It exists for API compatibility with the PHP Laravel collection counterpart. +Reports whether the given slice is a sequential list. In Go every slice is inherently a sequential, integer-indexed list, so this always returns `true`. ```go arr.IsList([]string{"a", "b", "c"}) // true arr.IsList([]int{}) // true ``` -**Why it is useful:** Allows code ported from Laravel's PHP collections to compile and behave correctly without modification. - --- -## First +## πŸ’Ž First ```go func First[T any](items []T, callbacks ...func(T, int) bool) (T, bool) ``` -Returns the first element of the slice, or the first element that matches the provided callback predicate. The second return value reports whether a match was found. +Returns the first element of the slice, or the first element that matches the provided callback predicate. ```go // Without a callback -- returns the first element. @@ -106,23 +102,17 @@ val, ok = arr.First([]int{1, 3, 4, 6}, func(v int, _ int) bool { return v%2 == 0 }) // val == 4, ok == true - -// Empty slice. -val, ok = arr.First([]int{}) -// val == 0, ok == false ``` -**Why it is useful:** Replaces the common `if len(s) > 0 { s[0] }` pattern with a single call that also supports predicate-based searching. - --- -## Last +## πŸ’Ž Last ```go func Last[T any](items []T, callbacks ...func(T, int) bool) (T, bool) ``` -Returns the last element of the slice, or the last element that matches the provided callback predicate. The second return value reports whether a match was found. +Returns the last element of the slice, or the last element that matches the provided callback predicate. ```go val, ok := arr.Last([]string{"a", "b", "c"}) @@ -134,11 +124,9 @@ val, ok = arr.Last([]int{1, 2, 3, 4, 5}, func(v int, _ int) bool { // val == 3, ok == true ``` -**Why it is useful:** Safely retrieves the last element (or last matching element) without manual length checks or reverse iteration. - --- -## Take +## πŸ’Ž Take ```go func Take[T any](items []T, limit int) []T @@ -149,14 +137,11 @@ Returns a new slice containing up to `limit` elements. A positive limit takes fr ```go arr.Take([]int{1, 2, 3, 4, 5}, 3) // [1, 2, 3] arr.Take([]int{1, 2, 3, 4, 5}, -2) // [4, 5] -arr.Take([]int{1, 2}, 10) // [1, 2] ``` -**Why it is useful:** Provides a single function for both "take first N" and "take last N" operations, handling edge cases automatically. - --- -## Only +## πŸ’Ž Only ```go func Only[T any](items []T, indices []int) []T @@ -167,16 +152,11 @@ Returns a new slice containing only the elements at the given indices. Out-of-ra ```go arr.Only([]string{"a", "b", "c", "d"}, []int{0, 2}) // ["a", "c"] - -arr.Only([]string{"a", "b"}, []int{0, 5}) -// ["a"] -- index 5 is out of range, skipped ``` -**Why it is useful:** Selects specific positions from a slice without manual bounds checking. - --- -## Except +## πŸ’Ž Except ```go func Except[T any](items []T, indices []int) []T @@ -189,11 +169,9 @@ arr.Except([]string{"a", "b", "c", "d"}, []int{1, 3}) // ["a", "c"] ``` -**Why it is useful:** The inverse of `Only` -- removes specific positions from a slice cleanly. - --- -## Flatten +## πŸ’Ž Flatten ```go func Flatten[T any](items [][]T) []T @@ -207,11 +185,9 @@ arr.Flatten(nested) // [1, 2, 3, 4, 5] ``` -**Why it is useful:** Eliminates nested loops when merging grouped results into a single list. - --- -## Collapse +## πŸ’Ž Collapse ```go func Collapse[T any](items [][]T) []T @@ -219,16 +195,9 @@ func Collapse[T any](items [][]T) []T Merges a slice of slices into a single slice. This is an alias for `Flatten`. -```go -arr.Collapse([][]string{{"a", "b"}, {"c"}}) -// ["a", "b", "c"] -``` - -**Why it is useful:** Provides an alternative name for `Flatten` that may read more naturally when the intent is to "collapse" grouped data. - --- -## Wrap +## πŸ’Ž Wrap ```go func Wrap[T any](value T) []T @@ -241,11 +210,9 @@ arr.Wrap(42) // [42] arr.Wrap("hello") // ["hello"] ``` -**Why it is useful:** Normalizes a single value into a slice so downstream code can always work with `[]T`. - --- -## WrapSlice +## πŸ’Ž WrapSlice ```go func WrapSlice[T any](value []T) []T @@ -253,16 +220,9 @@ func WrapSlice[T any](value []T) []T Returns the given slice unchanged. Use this when the value is already a slice and you want to avoid double-wrapping. -```go -s := []int{1, 2, 3} -arr.WrapSlice(s) // [1, 2, 3] -- same slice returned -``` - -**Why it is useful:** Pairs with `Wrap` to provide a uniform wrapping API that works whether the input is a single value or already a slice. - --- -## Prepend +## πŸ’Ž Prepend ```go func Prepend[T any](items []T, value T) []T @@ -275,11 +235,9 @@ arr.Prepend([]int{2, 3, 4}, 1) // [1, 2, 3, 4] ``` -**Why it is useful:** Go's built-in `append` adds to the end; `Prepend` provides the inverse operation in a single, readable call. - --- -## Push +## πŸ’Ž Push ```go func Push[T any](items []T, values ...T) []T @@ -287,16 +245,9 @@ func Push[T any](items []T, values ...T) []T Appends one or more values to the end of the slice and returns the new slice. -```go -arr.Push([]int{1, 2}, 3, 4, 5) -// [1, 2, 3, 4, 5] -``` - -**Why it is useful:** A thin wrapper around `append` that reads more naturally in collection-style code. - --- -## Shuffle +## πŸ’Ž Shuffle ```go func Shuffle[T any](items []T) []T @@ -304,109 +255,67 @@ func Shuffle[T any](items []T) []T Returns a new slice with the elements in random order. The original slice is not modified. -```go -arr.Shuffle([]int{1, 2, 3, 4, 5}) -// e.g. [3, 1, 5, 2, 4] -- random each time -``` - -**Why it is useful:** Provides an immutable shuffle without needing to set up a `rand.Shuffle` call manually. - --- -## Random +## πŸ’Ž Random ```go func Random[T any](items []T, counts ...int) []T ``` -Returns a new slice containing `count` randomly selected elements. If `count` is omitted it defaults to 1. If `count` exceeds the slice length, all elements are returned in random order. +Returns a new slice containing `count` randomly selected elements. Defaults to 1. ```go arr.Random([]string{"a", "b", "c", "d"}, 2) // e.g. ["c", "a"] - -arr.Random([]int{10, 20, 30}) -// e.g. [20] -- defaults to 1 ``` -**Why it is useful:** Quickly samples elements from a slice without writing shuffle-and-truncate boilerplate. - --- -## Sort +## πŸ’Ž Sort ```go func Sort[T any](items []T, less func(a, b T) bool) []T ``` -Returns a new slice sorted using the provided comparison function. The sort is stable (equal elements retain their original order). +Returns a new slice sorted using the provided comparison function. The sort is stable. ```go arr.Sort([]int{3, 1, 4, 1, 5}, func(a, b int) bool { return a < b }) // [1, 1, 3, 4, 5] - -type User struct { - Name string - Age int -} -users := []User{{Name: "Bob", Age: 30}, {Name: "Alice", Age: 25}} -arr.Sort(users, func(a, b User) bool { - return a.Age < b.Age -}) -// [{Alice 25}, {Bob 30}] ``` -**Why it is useful:** Returns a new sorted slice instead of sorting in place, which keeps the original data intact. - --- -## SortDesc +## πŸ’Ž SortDesc ```go func SortDesc[T any](items []T, less func(a, b T) bool) []T ``` -Returns a new slice sorted in descending order. The `less` function defines the ascending order; `SortDesc` reverses it internally. - -```go -arr.SortDesc([]int{3, 1, 4, 1, 5}, func(a, b int) bool { - return a < b -}) -// [5, 4, 3, 1, 1] -``` - -**Why it is useful:** Saves you from writing a reversed comparator when you need descending order. +Returns a new slice sorted in descending order. --- -## SortRecursive +## πŸ’Ž SortRecursive ```go func SortRecursive[T any](items []T, less func(a, b T) bool) []T ``` -Sorts a slice using the provided comparison function. For flat slices (which is the only kind in Go generics), this behaves identically to `Sort`. - -```go -arr.SortRecursive([]int{5, 2, 8, 1}, func(a, b int) bool { - return a < b -}) -// [1, 2, 5, 8] -``` - -**Why it is useful:** Exists for API parity with Laravel collections. Use `Sort` directly in new Go code. +Sorts a slice using the provided comparison function. Exists for API parity with Laravel collections. --- -## Where +## πŸ’Ž Where ```go func Where[T any](items []T, callback func(T, int) bool) []T ``` -Returns a new slice containing only the elements for which the callback returns `true`. The callback receives each element and its index. +Returns a new slice containing only the elements for which the callback returns `true`. ```go evens := arr.Where([]int{1, 2, 3, 4, 5, 6}, func(v int, _ int) bool { @@ -415,11 +324,9 @@ evens := arr.Where([]int{1, 2, 3, 4, 5, 6}, func(v int, _ int) bool { // [2, 4, 6] ``` -**Why it is useful:** A standard filter operation with access to both value and index -- the foundation for most data-selection patterns. - --- -## WhereNotNull +## πŸ’Ž WhereNotNull ```go func WhereNotNull[T comparable](items []T) []T @@ -430,16 +337,11 @@ Returns a new slice with all zero-value elements removed. The type must satisfy ```go arr.WhereNotNull([]string{"a", "", "b", "", "c"}) // ["a", "b", "c"] - -arr.WhereNotNull([]int{0, 1, 0, 2, 3}) -// [1, 2, 3] ``` -**Why it is useful:** Quickly strips empty strings, zero integers, nil pointers, or any other zero-value sentinel from a typed slice. - --- -## Reject +## πŸ’Ž Reject ```go func Reject[T any](items []T, callback func(T, int) bool) []T @@ -447,18 +349,9 @@ func Reject[T any](items []T, callback func(T, int) bool) []T Returns a new slice containing only the elements for which the callback returns `false`. It is the inverse of `Where`. -```go -arr.Reject([]int{1, 2, 3, 4, 5}, func(v int, _ int) bool { - return v > 3 -}) -// [1, 2, 3] -``` - -**Why it is useful:** Makes intent clearer when you want to express "remove items matching X" rather than "keep items not matching X." - --- -## Partition +## πŸ’Ž Partition ```go func Partition[T any](items []T, callback func(T, int) bool) ([]T, []T) @@ -470,63 +363,31 @@ Splits the slice into two: the first contains elements where the callback return pass, fail := arr.Partition([]int{1, 2, 3, 4, 5, 6}, func(v int, _ int) bool { return v%2 == 0 }) -// pass == [2, 4, 6] -// fail == [1, 3, 5] ``` -**Why it is useful:** Performs a single pass to separate elements into two groups, avoiding two separate filter calls. - --- -## Every +## πŸ’Ž Every ```go func Every[T any](items []T, callback func(T, int) bool) bool ``` -Reports whether every element in the slice satisfies the callback. Returns `true` for an empty slice. - -```go -arr.Every([]int{2, 4, 6}, func(v int, _ int) bool { - return v%2 == 0 -}) -// true - -arr.Every([]int{2, 3, 6}, func(v int, _ int) bool { - return v%2 == 0 -}) -// false -``` - -**Why it is useful:** Validates that all elements meet a condition without writing a manual loop with an early break. +Reports whether every element in the slice satisfies the callback. --- -## Some +## πŸ’Ž Some ```go func Some[T any](items []T, callback func(T, int) bool) bool ``` -Reports whether at least one element in the slice satisfies the callback. Returns `false` for an empty slice. - -```go -arr.Some([]int{1, 3, 5, 8}, func(v int, _ int) bool { - return v%2 == 0 -}) -// true - -arr.Some([]int{1, 3, 5}, func(v int, _ int) bool { - return v%2 == 0 -}) -// false -``` - -**Why it is useful:** Provides a short-circuiting existence check -- iteration stops as soon as a match is found. +Reports whether at least one element in the slice satisfies the callback. --- -## Exists +## πŸ’Ž Exists ```go func Exists[T any](items []T, index int) bool @@ -534,17 +395,9 @@ func Exists[T any](items []T, index int) bool Reports whether the given index is valid (in bounds) for the slice. -```go -arr.Exists([]string{"a", "b", "c"}, 2) // true -arr.Exists([]string{"a", "b", "c"}, 5) // false -arr.Exists([]string{"a", "b", "c"}, -1) // false -``` - -**Why it is useful:** A readable bounds check that replaces `index >= 0 && index < len(s)`. - --- -## Has +## πŸ’Ž Has ```go func Has[T any](items []T, indices ...int) bool @@ -552,16 +405,9 @@ func Has[T any](items []T, indices ...int) bool Reports whether **all** of the given indices are valid for the slice. -```go -arr.Has([]int{10, 20, 30}, 0, 1, 2) // true -arr.Has([]int{10, 20, 30}, 0, 5) // false -- 5 is out of range -``` - -**Why it is useful:** Validates multiple indices in one call before accessing them. - --- -## HasAny +## πŸ’Ž HasAny ```go func HasAny[T any](items []T, indices ...int) bool @@ -569,16 +415,9 @@ func HasAny[T any](items []T, indices ...int) bool Reports whether **at least one** of the given indices is valid for the slice. -```go -arr.HasAny([]int{10, 20, 30}, 5, 1) // true -- index 1 is valid -arr.HasAny([]int{10, 20, 30}, 5, 10) // false -- neither is valid -``` - -**Why it is useful:** A looser existence check than `Has`, useful when any one valid index is sufficient. - --- -## Join +## πŸ’Ž Join ```go func Join(items []string, glue string, finalGlues ...string) string @@ -587,41 +426,28 @@ func Join(items []string, glue string, finalGlues ...string) string Concatenates string slice elements with a glue string. An optional final glue is placed between the last two elements. ```go -arr.Join([]string{"a", "b", "c"}, ", ") -// "a, b, c" - arr.Join([]string{"a", "b", "c"}, ", ", " and ") // "a, b and c" - -arr.Join([]string{"Go"}, ", ", " and ") -// "Go" ``` -**Why it is useful:** Produces human-readable lists like "Alice, Bob and Charlie" without manual string building. - --- -## CrossJoin +## πŸ’Ž CrossJoin ```go func CrossJoin[T any](lists ...[]T) [][]T ``` -Returns the Cartesian product of the given slices. Each result element is a slice combining one element from each input. +Returns the Cartesian product of the given slices. ```go arr.CrossJoin([]int{1, 2}, []int{10, 20}) // [[1, 10], [1, 20], [2, 10], [2, 20]] - -arr.CrossJoin([]string{"S", "M"}, []string{"Red", "Blue"}) -// [["S", "Red"], ["S", "Blue"], ["M", "Red"], ["M", "Blue"]] ``` -**Why it is useful:** Generates all combinations across multiple dimensions (sizes, colors, options) without nested loops. - --- -## Divide +## πŸ’Ž Divide ```go func Divide[T any](items []T) ([]int, []T) @@ -629,41 +455,26 @@ func Divide[T any](items []T) ([]int, []T) Splits a slice into two: a slice of indices and a slice of values. -```go -indices, values := arr.Divide([]string{"a", "b", "c"}) -// indices == [0, 1, 2] -// values == ["a", "b", "c"] -``` - -**Why it is useful:** Separates positional information from data, useful when you need to work with indices and values independently. - --- -## Map +## πŸ’Ž Map ```go func Map[T any, R any](items []T, callback func(T, int) R) []R ``` -Applies the callback to each element and returns a new slice of the transformed results. The callback receives each element and its index. +Applies the callback to each element and returns a new slice of the transformed results. ```go arr.Map([]int{1, 2, 3}, func(v int, _ int) int { return v * 2 }) // [2, 4, 6] - -arr.Map([]string{"hello", "world"}, func(s string, _ int) int { - return len(s) -}) -// [5, 5] ``` -**Why it is useful:** The fundamental transform operation -- converts a slice of one type into a slice of another without manual loop construction. - --- -## MapWithKeys +## πŸ’Ž MapWithKeys ```go func MapWithKeys[T any, K comparable, V any](items []T, callback func(T) (K, V)) map[K]V @@ -671,71 +482,29 @@ func MapWithKeys[T any, K comparable, V any](items []T, callback func(T) (K, V)) Applies the callback to each element, which produces a key-value pair, and collects the results into a map. -```go -type User struct { - ID int - Name string -} - -users := []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}} -result := arr.MapWithKeys(users, func(u User) (int, string) { - return u.ID, u.Name -}) -// map[1:"Alice" 2:"Bob"] -``` - -**Why it is useful:** Converts a slice into a lookup map in a single expression, common when indexing records by a unique field. - --- -## MapSpread +## πŸ’Ž MapSpread ```go func MapSpread[T any, R any](items []T, callback func(T, int) R) []R ``` -Applies the callback to each element and returns a new slice. In Go this is identical to `Map`. - -```go -arr.MapSpread([]int{1, 2, 3}, func(v int, idx int) string { - return fmt.Sprintf("%d:%d", idx, v) -}) -// ["0:1", "1:2", "2:3"] -``` - -**Why it is useful:** Exists for API parity with Laravel collections. Use `Map` in new Go code. +Alias for `Map`. Exists for API parity with Laravel collections. --- -## KeyBy +## πŸ’Ž KeyBy ```go func KeyBy[T any, K comparable](items []T, keyFunc func(T) K) map[K]T ``` -Indexes the slice elements by the key returned from `keyFunc`, producing a map. If duplicate keys exist, the last element wins. - -```go -type Product struct { - SKU string - Name string -} - -products := []Product{ - {SKU: "A1", Name: "Widget"}, - {SKU: "B2", Name: "Gadget"}, -} -bysku := arr.KeyBy(products, func(p Product) string { - return p.SKU -}) -// map["A1":{SKU:"A1", Name:"Widget"} "B2":{SKU:"B2", Name:"Gadget"}] -``` - -**Why it is useful:** Creates a lookup table from a slice in one call, avoiding manual map construction loops. +Indexes the slice elements by the key returned from `keyFunc`, producing a map. --- -## Pluck +## πŸ’Ž Pluck ```go func Pluck[T any, V any](items []T, valueFunc func(T) V) []V @@ -758,11 +527,8 @@ names := arr.Pluck(employees, func(e Employee) string { return e.Name }) // ["Alice", "Bob"] - -salaries := arr.Pluck(employees, func(e Employee) float64 { - return e.Salary -}) -// [90000, 75000] ``` -**Why it is useful:** Extracts a column of data from a slice of structs, mirroring SQL's `SELECT column` pattern. +--- + +πŸ‘‰ [**Back to Overview**](overview.md) diff --git a/docs/collectible.md b/docs/collectible.md new file mode 100644 index 0000000..98b45b4 --- /dev/null +++ b/docs/collectible.md @@ -0,0 +1,128 @@ +# πŸ’Ž Collectible API Reference + +The `collectible` package provides a generic `Collection[K, V]` type that wraps a Go map. It maintains the insertion order of keys and offers a fluent API for common map operations, transformations, and queries. + +```go +import "github.com/gocanto/collection/collectible" +``` + +### πŸš€ Constructors + +| Function | Description | +| --- | --- | +| `New(items map[K]V)` | Creates a new Collection from a map. | +| `FromPairs(pairs ...Pair[K, V])` | Creates a Collection from key-value pairs. | + +### πŸ” Query & Access + +| Method | Description | +| --- | --- | +| `All()` | Returns a shallow copy of the underlying map. | +| `Keys()` | Returns all keys as a slice, preserving insertion order. | +| `Values()` | Returns all values as a slice, preserving insertion order. | +| `Count()` | Returns the number of items. | +| `IsEmpty()` | Reports whether the collection is empty. | +| `IsNotEmpty()` | Reports whether the collection is not empty. | +| `ContainsOneItem()` | Reports whether the collection has exactly one item. | +| `ContainsManyItems()` | Reports whether the collection has more than one item. | +| `Iter()` | Returns a standard Go iterator (`iter.Seq2[K, V]`). | +| `Get(key, defaults...)` | Returns the value for a key, with an optional default. | +| `Has(key)` | Reports whether a key exists. | +| `HasAny(keys...)` | Reports whether any of the given keys exist. | +| `Contains(predicate)` | Reports whether any item satisfies the predicate. | +| `Some(predicate)` | Alias for `Contains`. | +| `DoesntContain(predicate)` | Reports whether no item satisfies the predicate. | +| `HasSole(predicate)` | Reports whether exactly one item satisfies the predicate. | +| `Search(predicate)` | Returns the key of the first item satisfying the predicate. | +| `First(predicates...)` | Returns the first item matching the optional predicate. | +| `Last(predicates...)` | Returns the last item matching the optional predicate. | +| `Every(predicate)` | Reports whether all items satisfy the predicate. | + +### πŸ› οΈ Mutation & Transformation + +| Method / Function | Description | +| --- | --- | +| `Put(key, value)` | Sets a key-value pair in the collection. | +| `GetOrPut(key, value)` | Returns the value for a key or stores the default. | +| `Pull(key)` | Removes a key and returns its value. | +| `Forget(keys...)` | Removes one or more items by key. | +| `Merge(map)` | Returns a new collection with merged items (overwrites). | +| `Union(map)` | Returns a new collection with merged items (no overwrite). | +| `Replace(map)` | Alias for `Merge`. | +| `DiffKeys(map)` | Returns items whose keys are not in the given map. | +| `DiffKeysUsing(map, equals)` | `DiffKeys` with a custom comparison function. | +| `IntersectByKeys(map)` | Returns items whose keys are in the given map. | +| `Only(keys...)` | Returns a collection containing only the specified keys. | +| `Except(keys...)` | Returns a collection excluding the specified keys. | +| `Filter(predicate)` | Returns items satisfying the predicate. | +| `Reject(predicate)` | Returns items NOT satisfying the predicate. | +| `Partition(predicate)` | Splits the collection into two based on the predicate. | +| `MapValues(col, callback)` | (Function) Transforms values while keeping keys. | +| `Flip(col)` | (Function) Swaps keys and values. | + +### 🎯 Pipeline & Utilities + +| Method / Function | Description | +| --- | --- | +| `Each(callback)` | Iterates over items. Return `false` to stop. | +| `Tap(callback)` | Executes a callback with the collection and returns it. | +| `When(cond, callback, default)` | Applies a callback if the condition is true. | +| `Unless(cond, callback, default)` | Applies a callback unless the condition is true. | +| `Implode(glue)` | Concatenates values into a string. | +| `Join(glue, finalGlue)` | Joins values with a separator and optional final separator. | +| `ToJSON()` | Serializes the collection to JSON. | +| `Copy()` | Returns a shallow copy of the collection. | +| `ToPairs()` | Converts the collection to a slice of `Pair` values. | +| `Dump()` | Prints the items for debugging. | +| `DD()` | Prints the items and terminates the program. | + +### βš–οΈ Sorting + +| Method / Function | Description | +| --- | --- | +| `SortKeys(col)` | (Function) Sorts string keys in ascending order. | +| `SortKeysDesc(col)` | (Function) Sorts string keys in descending order. | +| `SortKeysUsing(less)` | Sorts keys using a custom comparison function. | + +--- + +## Detailed Methods + +### `Filter` +Filters the collection using the given callback. The callback receives the value and the key. + +```go +filtered := col.Filter(func(v int, k string) bool { + return v > 10 +}) +``` + +### `MapValues` +Transforms each value in the collection. Note that this is a package-level function to allow changing the value type. + +```go +names := collectible.New(map[int]string{1: "juan", 2: "pedro"}) +lengths := collectible.MapValues(names, func(v string, k int) int { + return len(v) +}) +``` + +### `Merge` & `Union` +`Merge` overwrites existing keys, while `Union` only adds keys that are not already present. + +```go +col := collectible.New(map[string]int{"a": 1}) +merged := col.Merge(map[string]int{"a": 2, "b": 3}) // a: 2, b: 3 +union := col.Union(map[string]int{"a": 3, "c": 4}) // a: 1, c: 4 +``` + +### `Partition` +Splits the collection into two collections: one containing items that pass the truth test, and another containing those that don't. + +```go +underage, adults := users.Partition(func(u User, id string) bool { + return u.Age < 18 +}) +``` + +πŸ‘‰ [**Back to Overview**](overview.md) diff --git a/docs/collection.md b/docs/collection.md index 97509a2..6f0fcf6 100644 --- a/docs/collection.md +++ b/docs/collection.md @@ -1,2776 +1,291 @@ -# Collection[T] API Reference - -`Collection[T]` is a generic wrapper around a Go slice that provides a fluent, chainable API for filtering, sorting, transforming, and aggregating data. It is the core type of the `github.com/gocanto/collection/collection` package. - -```go -import "github.com/gocanto/collection/collection" -``` - -> **A note on top-level functions vs methods.** Go generics do not allow methods to introduce new type parameters. Functions like `Map`, `FlatMap`, `Reduce`, `SortBy`, `Unique`, `Duplicates`, `Pluck`, `GroupBy`, `KeyBy`, `CountBy`, `Zip`, `CrossJoin`, `Combine`, `Collapse`, `Diff`, `Intersect`, `Pipe`, `PipeInto`, `Sum`, `SumBy`, `Avg`, `AvgBy`, `Average`, `Min`, `MinBy`, `Max`, `MaxBy`, `Median`, `MedianBy`, `Mode`, `MapToDictionary`, `MapToGroups`, `MapWithKeys`, and `MapInto` are therefore package-level functions, not methods. Call them as `collection.Map(c, fn)`, not `c.Map(fn)`. - ---- - -## Constructors +# πŸ’Ž Collection[T] API Reference + +`import "github.com/gocanto/collection/collection"` + +`Collection[T]` is a generic wrapper around a Go slice that provides a fluent, chainable API for filtering, sorting, transforming, and aggregating data. It is the core type of the `collection` package. + +> **⚠️ Note on Generics:** Go generics do not allow methods to introduce new type parameters. Functions like `Map`, `Reduce`, `Pluck`, and `GroupBy` are implemented as package-level functions (e.g., `collection.Map(c, fn)`), not methods. + +--- + +## πŸ›  Available Functions + +### Constructors & Query +| Function | Purpose | +|:---|:---| +| [**New**](#new) | Creates a new collection from variadic arguments. | +| [**Collect**](#collect) | Creates a new collection from an existing slice. | +| [**Empty**](#empty) | Creates an empty collection of the given type. | +| [**Wrap**](#wrap) | Wraps any value into a collection. | +| [**Unwrap**](#unwrap) | Extracts the underlying slice from a collection. | +| [**Times**](#times) | Creates a collection by invoking a callback N times. | +| [**Range**](#range) | Creates a collection of consecutive integers. | +| [**All**](#all) | Returns the underlying slice. | +| [**Count**](#count) | Returns the number of items in the collection. | +| [**IsEmpty**](#isempty) | Reports whether the collection is empty. | +| [**IsNotEmpty**](#isnotempty) | Reports whether the collection is not empty. | +| [**ContainsOneItem**](#containsoneitem) | Reports if there is exactly one item. | +| [**ContainsManyItems**](#containsmanyitems) | Reports if there are multiple items. | +| [**Has**](#has) | Reports whether a given index exists. | + +### Retrieval & Search +| Function | Purpose | +|:---|:---| +| [**First**](#first) | Returns the first item matching a predicate. | +| [**FirstOrFail**](#firstorfail) | Returns the first item or an error if not found. | +| [**Last**](#last) | Returns the last item matching a predicate. | +| [**Sole**](#sole) | Returns the only item matching a predicate (or error). | +| [**Get**](#get) | Returns the item at a given index (with optional default). | +| [**Search**](#search) | Returns the index of the first matching item. | +| [**Contains**](#contains) | Reports if any item satisfies a predicate. | +| [**Before**](#before) | Returns the item before the first match. | +| [**After**](#after) | Returns the item after the first match. | + +### Mutation & Iteration +| Function | Purpose | +|:---|:---| +| [**Push**](#push) / [**Add**](#add) | Appends items to the collection (mutates). | +| [**Prepend**](#prepend) | Adds an item to the beginning (mutates). | +| [**Pop**](#pop) | Removes and returns the last item. | +| [**Shift**](#shift) | Removes and returns the first item. | +| [**Put**](#put) | Sets the item at a given index. | +| [**Pull**](#pull) | Removes and returns an item by index. | +| [**Forget**](#forget) | Removes an item by index (mutates). | +| [**Each**](#each) | Iterates over items with a callback. | +| [**Tap**](#tap) | Passes the collection to a callback for side effects. | +| [**Iter**](#iter) | Returns a Go 1.23+ iterator (`iter.Seq[T]`). | + +### Transformation & Sorting +| Function | Purpose | +|:---|:---| +| [**Map**](#map) | Transforms items into a new type (Package-level). | +| [**FlatMap**](#flatmap) | Maps and flattens results (Package-level). | +| [**Reduce**](#reduce) | Accumulates a single result (Package-level). | +| [**Filter**](#filter) / [**Where**](#where) | Returns items matching a predicate. | +| [**Unique**](#unique) | Returns distinct items (Package-level). | +| [**Sort**](#sort) / [**SortBy**](#sortby) | Returns a new sorted collection. | +| [**Reverse**](#reverse) | Returns a reversed collection. | +| [**Shuffle**](#shuffle) | Returns a randomized collection. | + +### Partitioning & Slicing +| Function | Purpose | +|:---|:---| +| [**Chunk**](#chunk) | Breaks the collection into multiple slices of N size. | +| [**Split**](#split) | Splits items into a fixed number of groups. | +| [**Slice**](#slice) | Extracts a portion of the collection. | +| [**Take**](#take) | Returns N items from the front or back. | +| [**Skip**](#skip) | Skips N items and returns the rest. | +| [**ForPage**](#forpage) | Returns a subset for a specific page. | + +### Combining & Aggregation +| Function | Purpose | +|:---|:---| +| [**Concat**](#concat) / [**Merge**](#merge) | Joins two datasets. | +| [**Zip**](#zip) | Merges multiple slices element-by-element. | +| [**Sum**](#sum) / [**Avg**](#avg) | Computes totals or averages (Package-level). | +| [**Min**](#min) / [**Max**](#max) | Finds minimum or maximum values (Package-level). | +| [**GroupBy**](#groupby) | Groups items into a map of collections. | + +--- + +## πŸš€ Constructors ### New - ```go func New[T any](items ...T) *Collection[T] ``` - Creates a new collection from variadic arguments. -```go -nums := collection.New(1, 2, 3) -fmt.Println(nums.Count()) // 3 -``` - -Use `New` when you have a handful of literal values to wrap. - ---- - ### Collect - ```go func Collect[T any](items []T) *Collection[T] ``` - Creates a new collection from an existing slice. -```go -names := []string{"Alice", "Bob", "Carol"} -c := collection.Collect(names) -fmt.Println(c.Count()) // 3 -``` - -Use `Collect` when you already have a slice from another function or API call. - ---- - ### Empty - ```go func Empty[T any]() *Collection[T] ``` - Creates an empty collection of the given type. -```go -c := collection.Empty[string]() -fmt.Println(c.IsEmpty()) // true -``` - -Useful as a starting point when you plan to build up a collection with `Push`. - ---- - -### Wrap - -```go -func Wrap[T any](value any) *Collection[T] -``` - -Wraps a value in a collection. If the value is already a `*Collection[T]`, it is returned as-is. If it is a `[]T`, it is wrapped. Otherwise a single-element collection is created. - -```go -c := collection.Wrap[int]([]int{10, 20}) -fmt.Println(c.Count()) // 2 -``` - -Useful at API boundaries where the input type is not known statically. - ---- - -### Unwrap - -```go -func Unwrap[T any](value any) []T -``` - -Extracts the underlying slice from a collection or returns the value itself if it is already a slice. Returns `nil` if the value is neither. - -```go -c := collection.New(1, 2, 3) -items := collection.Unwrap[int](c) -fmt.Println(len(items)) // 3 -``` - -Useful when you need to pass collection data to functions that expect plain slices. - ---- - -### Times - -```go -func Times[T any](number int, callback func(int) T) *Collection[T] -``` - -Creates a collection by invoking the callback `number` times. The callback receives a 1-based index. - -```go -labels := collection.Times(3, func(i int) string { - return fmt.Sprintf("Item #%d", i) -}) -// ["Item #1", "Item #2", "Item #3"] -``` - -Useful for generating seed data, placeholder records, or numbered sequences. - ---- - -### Range - -```go -func Range(from, to int) *Collection[int] -``` - -Creates a collection of consecutive integers from `from` to `to` (inclusive). If `from > to`, the sequence counts downward. - -```go -ascending := collection.Range(1, 5) // [1, 2, 3, 4, 5] -descending := collection.Range(5, 1) // [5, 4, 3, 2, 1] -``` - -Useful for generating index ranges, pagination offsets, or numeric test data. - --- -## Query +## πŸ” Query ### All - ```go func (c *Collection[T]) All() []T ``` - -Returns the underlying slice. This does not copy the data. - -```go -c := collection.New("a", "b", "c") -items := c.All() -fmt.Println(items) // [a b c] -``` - -Use `All` when you need direct access to the underlying data for interop with slice-based APIs. - ---- +Returns the underlying slice. ### Count - ```go func (c *Collection[T]) Count() int ``` - Returns the number of items in the collection. -```go -c := collection.New(10, 20, 30) -fmt.Println(c.Count()) // 3 -``` - -Use `Count` for size checks, loop bounds, or conditional logic based on collection size. - ---- - -### IsEmpty - +### IsEmpty / IsNotEmpty ```go func (c *Collection[T]) IsEmpty() bool -``` - -Reports whether the collection contains no items. - -```go -c := collection.Empty[int]() -if c.IsEmpty() { - fmt.Println("nothing here") -} -``` - -Use `IsEmpty` for guard clauses before processing a collection. - ---- - -### IsNotEmpty - -```go func (c *Collection[T]) IsNotEmpty() bool ``` -Reports whether the collection contains at least one item. - -```go -results := collection.New("found one") -if results.IsNotEmpty() { - fmt.Println("we have results") -} -``` - -Use `IsNotEmpty` as a readable alternative to `c.Count() > 0`. - ---- - -### ContainsOneItem - -```go -func (c *Collection[T]) ContainsOneItem() bool -``` - -Reports whether the collection contains exactly one item. - -```go -c := collection.New("only") -fmt.Println(c.ContainsOneItem()) // true -``` - -Useful for validating that a query returned a singular result. - ---- - -### ContainsManyItems - -```go -func (c *Collection[T]) ContainsManyItems() bool -``` - -Reports whether the collection contains more than one item. - -```go -c := collection.New(1, 2, 3) -fmt.Println(c.ContainsManyItems()) // true -``` - -Useful when you need to distinguish between single-item and multi-item results. - ---- - -### HasMany - -```go -func (c *Collection[T]) HasMany() bool -``` - -Alias for `ContainsManyItems`. - ---- - -### Has - -```go -func (c *Collection[T]) Has(index int) bool -``` - -Reports whether the given index exists. Negative indices count from the end. - -```go -c := collection.New("a", "b", "c") -fmt.Println(c.Has(1)) // true -fmt.Println(c.Has(-1)) // true (last element) -fmt.Println(c.Has(10)) // false -``` - -Use `Has` to safely check bounds before calling `Get`. - ---- - -### HasAny - -```go -func (c *Collection[T]) HasAny(indices ...int) bool -``` - -Reports whether any of the given indices exist in the collection. - -```go -c := collection.New("a", "b") -fmt.Println(c.HasAny(0, 5, 10)) // true (index 0 exists) -``` - -Useful for checking whether at least one of several expected positions is populated. - ---- - -### Keys - -```go -func (c *Collection[T]) Keys() *Collection[int] -``` - -Returns a collection of indices (0 through n-1). - -```go -c := collection.New("a", "b", "c") -keys := c.Keys() // [0, 1, 2] -``` - -Useful when you need the index set for further computation. - --- -## Retrieval +## 🎯 Retrieval ### First - ```go func (c *Collection[T]) First(predicates ...func(T, int) bool) (T, bool) ``` +Returns the first matching element. -Returns the first element matching the optional predicate. If no predicate is given, returns the first element. The second return value indicates whether a match was found. - +### Get ```go -type User struct { - Name string - Age int -} - -users := collection.New( - User{"Alice", 30}, - User{"Bob", 25}, - User{"Carol", 35}, -) - -// First element overall -first, ok := users.First() -fmt.Println(first.Name, ok) // Alice true - -// First user over 28 -senior, ok := users.First(func(u User, _ int) bool { - return u.Age > 28 -}) -fmt.Println(senior.Name, ok) // Alice true +func (c *Collection[T]) Get(index int, defaults ...T) (T, bool) ``` - -Use `First` for quick lookups without scanning the full collection. +Returns the item at the given index with an optional default value. --- -### FirstOrFail - -```go -func (c *Collection[T]) FirstOrFail(predicates ...func(T, int) bool) (T, error) -``` - -Like `First`, but returns an `ItemNotFoundError` instead of `false` when no match is found. +## πŸ§ͺ Search +### Contains ```go -user, err := users.FirstOrFail(func(u User, _ int) bool { - return u.Name == "Dave" -}) -if err != nil { - fmt.Println(err) // "item not found" -} +func (c *Collection[T]) Contains(predicate func(T, int) bool) bool ``` - -Use `FirstOrFail` when a missing item should be treated as an error rather than silently ignored. +Reports whether any item satisfies the predicate. --- -### Last +## πŸ›  Mutation +### Push / Add ```go -func (c *Collection[T]) Last(predicates ...func(T, int) bool) (T, bool) +func (c *Collection[T]) Push(values ...T) *Collection[T] ``` +Appends items to the collection. Mutates in place. -Returns the last element matching the optional predicate. If no predicate is given, returns the last element. - +### Forget ```go -c := collection.New(1, 2, 3, 4, 5) -last, _ := c.Last() -fmt.Println(last) // 5 - -lastEven, _ := c.Last(func(n int, _ int) bool { - return n%2 == 0 -}) -fmt.Println(lastEven) // 4 +func (c *Collection[T]) Forget(index int) *Collection[T] ``` - -Use `Last` to find the most recent or final matching element. +Removes an item by index. Mutates in place. --- -### Sole +## ♻️ Iteration +### Each ```go -func (c *Collection[T]) Sole(predicates ...func(T, int) bool) (T, error) +func (c *Collection[T]) Each(callback func(T, int) bool) *Collection[T] ``` +Iterates over items. Return `false` to stop early. -Returns the only element matching the optional predicate. Returns `ItemNotFoundError` if zero items match, or `MultipleItemsFoundError` if more than one matches. - +### Iter ```go -admins := collection.New( - User{"Alice", 30}, -) -admin, err := admins.Sole() -fmt.Println(admin.Name, err) // Alice - -// If multiple items match: -all := collection.New(User{"A", 1}, User{"B", 2}) -_, err = all.Sole() -fmt.Println(err) // "multiple items found: 2 items" +func (c *Collection[T]) Iter() iter.Seq[T] ``` - -Use `Sole` when your logic requires exactly one matching record and any other count is an error. +Returns a Go 1.23+ iterator compatible with `for range`. --- -### HasSole +## πŸ’Ž Transformation (Package-Level) +### Map ```go -func (c *Collection[T]) HasSole(predicates ...func(T, int) bool) bool +func Map[T any, R any](c *Collection[T], callback func(T, int) R) *Collection[R] ``` +Transforms each item into a new type. -Reports whether exactly one item matches the optional predicate. - +### Reduce ```go -c := collection.New(1, 2, 3) -fmt.Println(c.HasSole(func(n int, _ int) bool { return n > 2 })) // true (only 3) +func Reduce[T any, R any](c *Collection[T], callback func(R, T, int) R, initial R) R ``` - -Use `HasSole` as a boolean check before calling `Sole` to avoid error handling. +Reduces the collection to a single value. --- -### Get +## 🧹 Filtering +### Filter / Where ```go -func (c *Collection[T]) Get(index int, defaults ...T) (T, bool) +func (c *Collection[T]) Filter(callback func(T, int) bool) *Collection[T] ``` +Returns items matching the predicate. -Returns the item at the given index. Negative indices count from the end. The second return value indicates whether the index was in bounds. An optional default value can be provided. - +### Unique ```go -c := collection.New("a", "b", "c") -val, ok := c.Get(1) -fmt.Println(val, ok) // b true - -val, ok = c.Get(-1) -fmt.Println(val, ok) // c true - -val, ok = c.Get(99, "fallback") -fmt.Println(val, ok) // fallback false +func Unique[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] ``` - -Use `Get` for safe index-based access with optional defaults. +Returns a new collection with distinct keys. --- -### GetOrPut - -```go -func (c *Collection[T]) GetOrPut(index int, value T) T -``` - -Returns the item at the given index if it exists. Otherwise appends the value to the collection and returns it. +## πŸ“Š Sorting +### Sort ```go -c := collection.New("a", "b") -val := c.GetOrPut(0, "default") // "a" (exists) -val = c.GetOrPut(5, "new") // "new" (appended) -fmt.Println(c.Count()) // 3 +func (c *Collection[T]) Sort(less func(a, b T) bool) *Collection[T] ``` - -Useful for "get or initialize" patterns. +Returns a new collection sorted by the provided function. --- -## Search - -### Contains - -```go -func (c *Collection[T]) Contains(predicate func(T, int) bool) bool -``` - -Reports whether any item satisfies the predicate. +## 🍰 Partitioning +### Chunk ```go -c := collection.New(1, 2, 3, 4, 5) -hasEven := c.Contains(func(n int, _ int) bool { - return n%2 == 0 -}) -fmt.Println(hasEven) // true +func (c *Collection[T]) Chunk(size int) [][]T ``` - -Use `Contains` to check for the existence of an item matching a condition. +Breaks the collection into multiple slices of the given size. --- -### Some +## βœ‚οΈ Slicing +### Take ```go -func (c *Collection[T]) Some(predicate func(T, int) bool) bool +func (c *Collection[T]) Take(limit int) *Collection[T] ``` - -Alias for `Contains`. +Returns N items from the front (positive) or back (negative). --- -### DoesntContain - -```go -func (c *Collection[T]) DoesntContain(predicate func(T, int) bool) bool -``` - -Reports whether no item satisfies the predicate. The logical inverse of `Contains`. +## βž• Combining +### Merge ```go -c := collection.New(1, 3, 5) -noEvens := c.DoesntContain(func(n int, _ int) bool { - return n%2 == 0 -}) -fmt.Println(noEvens) // true +func (c *Collection[T]) Merge(items []T) *Collection[T] ``` - -Use `DoesntContain` for readable "none match" checks. +Joins the collection with another slice. --- -### Search +## πŸ“ˆ Aggregation (Package-Level) +### Sum / Avg ```go -func (c *Collection[T]) Search(predicate func(T, int) bool) (int, bool) +func Sum[T Numeric](c *Collection[T]) T +func Avg[T Numeric](c *Collection[T]) float64 ``` -Returns the index of the first item satisfying the predicate. The second return value indicates whether a match was found. - +### Min / Max ```go -c := collection.New("apple", "banana", "cherry") -idx, found := c.Search(func(s string, _ int) bool { - return s == "banana" -}) -fmt.Println(idx, found) // 1 true +func Min[T cmp.Ordered](c *Collection[T]) (T, bool) +func Max[T cmp.Ordered](c *Collection[T]) (T, bool) ``` -Use `Search` when you need the position of a matching item. - --- -### Before - -```go -func (c *Collection[T]) Before(predicate func(T, int) bool) (T, bool) -``` - -Returns the item immediately before the first item matching the predicate. +## πŸ“¦ Serialization +### ToJSON / ToPrettyJSON ```go -c := collection.New("a", "b", "c", "d") -before, ok := c.Before(func(s string, _ int) bool { - return s == "c" -}) -fmt.Println(before, ok) // b true +func (c *Collection[T]) ToJSON() ([]byte, error) +func (c *Collection[T]) ToPrettyJSON() ([]byte, error) ``` -Useful for getting the predecessor of a known element, such as finding the previous step in a workflow. - --- -### After - -```go -func (c *Collection[T]) After(predicate func(T, int) bool) (T, bool) -``` - -Returns the item immediately after the first item matching the predicate. - -```go -c := collection.New("a", "b", "c", "d") -after, ok := c.After(func(s string, _ int) bool { - return s == "b" -}) -fmt.Println(after, ok) // c true -``` - -Useful for getting the successor of a known element, such as finding the next step in a pipeline. - ---- - -## Mutation - -### Push - -```go -func (c *Collection[T]) Push(values ...T) *Collection[T] -``` - -Appends one or more items to the end of the collection. Mutates the collection in place. - -```go -c := collection.New(1, 2) -c.Push(3, 4) -fmt.Println(c.All()) // [1 2 3 4] -``` - -Use `Push` to add items to a growing collection. - ---- - -### Add - -```go -func (c *Collection[T]) Add(item T) *Collection[T] -``` - -Appends a single item. Alias for `Push` with one argument. - -```go -c := collection.New("x") -c.Add("y") -fmt.Println(c.All()) // [x y] -``` - ---- - -### Prepend - -```go -func (c *Collection[T]) Prepend(value T) *Collection[T] -``` - -Adds an item to the beginning of the collection. - -```go -c := collection.New(2, 3) -c.Prepend(1) -fmt.Println(c.All()) // [1 2 3] -``` - -Use `Prepend` when insertion order matters and you need the new item at the front. - ---- - -### Unshift - -```go -func (c *Collection[T]) Unshift(value T) *Collection[T] -``` - -Alias for `Prepend`. - ---- - -### Pop - -```go -func (c *Collection[T]) Pop(counts ...int) (T, bool) -``` - -Removes and returns the last item from the collection. The second return value indicates whether the collection was non-empty. - -```go -c := collection.New(1, 2, 3) -last, ok := c.Pop() -fmt.Println(last, ok) // 3 true -fmt.Println(c.All()) // [1 2] -``` - -Use `Pop` for stack-like (LIFO) behavior. - ---- - -### PopMany - -```go -func (c *Collection[T]) PopMany(count int) *Collection[T] -``` - -Removes and returns the last `count` items from the collection as a new collection. - -```go -c := collection.New(1, 2, 3, 4, 5) -popped := c.PopMany(2) -fmt.Println(popped.All()) // [4 5] -fmt.Println(c.All()) // [1 2 3] -``` - -Use `PopMany` to remove a batch of items from the end. - ---- - -### Shift - -```go -func (c *Collection[T]) Shift() (T, bool) -``` - -Removes and returns the first item from the collection. - -```go -c := collection.New("a", "b", "c") -first, ok := c.Shift() -fmt.Println(first, ok) // a true -fmt.Println(c.All()) // [b c] -``` - -Use `Shift` for queue-like (FIFO) behavior. - ---- - -### ShiftMany - -```go -func (c *Collection[T]) ShiftMany(count int) *Collection[T] -``` - -Removes and returns the first `count` items as a new collection. - -```go -c := collection.New(1, 2, 3, 4, 5) -shifted := c.ShiftMany(2) -fmt.Println(shifted.All()) // [1 2] -fmt.Println(c.All()) // [3 4 5] -``` - -Use `ShiftMany` to dequeue a batch of items. - ---- - -### Put - -```go -func (c *Collection[T]) Put(index int, value T) *Collection[T] -``` - -Sets the item at the given index to the given value. Does nothing if the index is out of bounds. - -```go -c := collection.New("a", "b", "c") -c.Put(1, "B") -fmt.Println(c.All()) // [a B c] -``` - -Use `Put` for in-place updates at a known position. - ---- - -### Pull - -```go -func (c *Collection[T]) Pull(index int) (T, bool) -``` - -Removes and returns the item at the given index. The second return value indicates whether the index was valid. - -```go -c := collection.New(10, 20, 30) -val, ok := c.Pull(1) -fmt.Println(val, ok) // 20 true -fmt.Println(c.All()) // [10 30] -``` - -Use `Pull` to remove a specific element by position. - ---- - -### Forget - -```go -func (c *Collection[T]) Forget(index int) *Collection[T] -``` - -Removes an item by index, mutating the collection. Returns the collection for chaining. - -```go -c := collection.New("a", "b", "c") -c.Forget(1) -fmt.Println(c.All()) // [a c] -``` - -Use `Forget` when you do not need the removed value. - ---- - -### Transform - -```go -func (c *Collection[T]) Transform(callback func(T, int) T) *Collection[T] -``` - -Applies the callback to each item in place, mutating the collection. Unlike `Map`, the return type must be the same as the input type. - -```go -prices := collection.New(10.0, 20.0, 30.0) -prices.Transform(func(p float64, _ int) float64 { - return p * 1.1 // apply 10% markup -}) -fmt.Println(prices.All()) // [11 22 33] -``` - -Use `Transform` for in-place mutations when you do not need a new type. - ---- - -## Iteration - -### Each - -```go -func (c *Collection[T]) Each(callback func(T, int) bool) *Collection[T] -``` - -Iterates over items, passing each item and its index to the callback. Return `false` from the callback to stop early. - -```go -collection.New("a", "b", "c").Each(func(item string, i int) bool { - fmt.Printf("%d: %s\n", i, item) - return true // continue -}) -``` - -Use `Each` for side effects like logging, sending notifications, or accumulating into external state. - ---- - -### EachSpread - -```go -func (c *Collection[T]) EachSpread(callback func(T, int) bool) *Collection[T] -``` - -Operates the same as `Each` in Go. Exists for API parity with the Laravel collection. - ---- - -### Tap - -```go -func (c *Collection[T]) Tap(callback func(*Collection[T])) *Collection[T] -``` - -Passes the collection to the callback for side effects and returns the collection unchanged. - -```go -result := collection.New(1, 2, 3). - Tap(func(c *collection.Collection[int]) { - fmt.Println("Count:", c.Count()) - }). - Filter(func(n int, _ int) bool { return n > 1 }) -``` - -Use `Tap` to inspect or log a collection mid-chain without breaking the fluent flow. - ---- - -### TapEach - -```go -func (c *Collection[T]) TapEach(callback func(T, int)) *Collection[T] -``` - -Calls the callback on each item for side effects, returning the original collection unchanged. - -```go -collection.New("order-1", "order-2").TapEach(func(id string, _ int) { - fmt.Println("Processing:", id) -}) -``` - -Use `TapEach` to perform side effects on each item (logging, metrics) without modifying the collection. - ---- - -### Pipe - -```go -func Pipe[T any, R any](c *Collection[T], callback func(*Collection[T]) R) R -``` - -Passes the collection to the callback and returns the callback's result. The result can be any type. - -```go -c := collection.New(1, 2, 3, 4, 5) -sum := collection.Pipe(c, func(c *collection.Collection[int]) int { - total := 0 - c.Each(func(n int, _ int) bool { total += n; return true }) - return total -}) -fmt.Println(sum) // 15 -``` - -Use `Pipe` to break out of the fluent chain with a custom computation that returns a different type. - ---- - -### PipeInto - -```go -func PipeInto[T any, R any](c *Collection[T], constructor func(*Collection[T]) R) R -``` - -Passes the collection to a constructor function and returns the result. Semantically identical to `Pipe` but communicates the intent of constructing a new value. - -```go -type Report struct{ Total int } - -c := collection.New(100, 200, 300) -report := collection.PipeInto(c, func(c *collection.Collection[int]) Report { - return Report{Total: c.Count()} -}) -``` - -Use `PipeInto` when you want to construct a domain object from a collection. - ---- - -### PipeThrough - -```go -func PipeThrough[T any](c *Collection[T], callbacks ...func(*Collection[T]) *Collection[T]) *Collection[T] -``` - -Passes the collection through a series of callbacks, returning the final result. Each callback receives and returns a `*Collection[T]`. - -```go -result := collection.PipeThrough( - collection.New(1, 2, 3, 4, 5), - func(c *collection.Collection[int]) *collection.Collection[int] { - return c.Filter(func(n int, _ int) bool { return n > 2 }) - }, - func(c *collection.Collection[int]) *collection.Collection[int] { - return c.Reverse() - }, -) -fmt.Println(result.All()) // [5 4 3] -``` - -Use `PipeThrough` to build reusable processing pipelines from composable stages. - ---- - -### Iter - -```go -func (c *Collection[T]) Iter() iter.Seq[T] -``` - -Returns a Go 1.23+ iterator (`iter.Seq[T]`) that yields each item. Compatible with range-over-func loops. - -```go -c := collection.New("x", "y", "z") -for item := range c.Iter() { - fmt.Println(item) -} -``` - -Use `Iter` to integrate collections with the standard iterator protocol. - ---- - -### Iter2 - -```go -func (c *Collection[T]) Iter2() iter.Seq2[int, T] -``` - -Returns a Go 1.23+ iterator that yields each index-item pair. - -```go -c := collection.New("a", "b", "c") -for i, item := range c.Iter2() { - fmt.Printf("%d: %s\n", i, item) -} -``` - -Use `Iter2` when you need both index and value in a range loop. - ---- - -## Filtering - -### Filter - -```go -func (c *Collection[T]) Filter(callback func(T, int) bool) *Collection[T] -``` - -Returns a new collection containing only items for which the callback returns `true`. - -```go -type Order struct { - ID int - Amount float64 -} - -orders := collection.New( - Order{1, 50.0}, - Order{2, 150.0}, - Order{3, 25.0}, -) -big := orders.Filter(func(o Order, _ int) bool { - return o.Amount > 100 -}) -fmt.Println(big.Count()) // 1 -``` - -Use `Filter` for any "keep matching items" operation. - ---- - -### Reject - -```go -func (c *Collection[T]) Reject(callback func(T, int) bool) *Collection[T] -``` - -Returns a new collection excluding items for which the callback returns `true`. The inverse of `Filter`. - -```go -nums := collection.New(1, 2, 3, 4, 5) -odds := nums.Reject(func(n int, _ int) bool { - return n%2 == 0 -}) -fmt.Println(odds.All()) // [1 3 5] -``` - -Use `Reject` when it is more natural to describe what you want to remove rather than what you want to keep. - ---- - -### Where - -```go -func (c *Collection[T]) Where(predicate func(T) bool) *Collection[T] -``` - -Filters items using a predicate that receives only the item (no index). A convenience wrapper around `Filter`. - -```go -type Product struct { - Name string - InStock bool -} - -products := collection.New( - Product{"Widget", true}, - Product{"Gadget", false}, - Product{"Doohickey", true}, -) -available := products.Where(func(p Product) bool { - return p.InStock -}) -fmt.Println(available.Count()) // 2 -``` - -Use `Where` when you do not need the index in your predicate, for cleaner call sites. - ---- - -### WhereNot - -```go -func (c *Collection[T]) WhereNot(predicate func(T) bool) *Collection[T] -``` - -Filters items using a negative predicate. Keeps items for which the predicate returns `false`. - -```go -discontinued := products.WhereNot(func(p Product) bool { - return p.InStock -}) -fmt.Println(discontinued.Count()) // 1 -``` - -Use `WhereNot` as the inverse of `Where`. - ---- - -### Unique - -```go -func Unique[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] -``` - -Returns a new collection containing only items with distinct keys as determined by the key function. - -```go -type User struct { - Email string - Name string -} - -users := collection.New( - User{"a@co.com", "Alice"}, - User{"b@co.com", "Bob"}, - User{"a@co.com", "Alice Duplicate"}, -) -unique := collection.Unique(users, func(u User) string { - return u.Email -}) -fmt.Println(unique.Count()) // 2 -``` - -Use `Unique` to deduplicate records by a key field. - ---- - -### Duplicates - -```go -func Duplicates[T any, K comparable](c *Collection[T], keyFunc func(T) K) *Collection[T] -``` - -Returns a new collection containing items that appear more than once, as determined by the key function. - -```go -tags := collection.New("go", "rust", "go", "python", "rust") -dups := collection.Duplicates(tags, func(s string) string { return s }) -fmt.Println(dups.All()) // [go rust] -``` - -Use `Duplicates` to detect and inspect repeated entries in a dataset. - ---- - -## Transformation - -### Map - -```go -func Map[T any, R any](c *Collection[T], callback func(T, int) R) *Collection[R] -``` - -Applies the callback to each item and returns a new collection of the results. The output type can differ from the input type. - -```go -type User struct { - Name string - Age int -} - -users := collection.New(User{"Alice", 30}, User{"Bob", 25}) -names := collection.Map(users, func(u User, _ int) string { - return u.Name -}) -fmt.Println(names.All()) // [Alice Bob] -``` - -Use `Map` whenever you need to transform items into a different type. - ---- - -### FlatMap - -```go -func FlatMap[T any, R any](c *Collection[T], callback func(T, int) []R) *Collection[R] -``` - -Applies the callback to each item (which returns a slice), then flattens the results into a single collection. - -```go -sentences := collection.New("hello world", "foo bar") -words := collection.FlatMap(sentences, func(s string, _ int) []string { - return strings.Split(s, " ") -}) -fmt.Println(words.All()) // [hello world foo bar] -``` - -Use `FlatMap` when each input item maps to multiple output items. - ---- - -### MapInto - -```go -func MapInto[T any, R any](c *Collection[T], constructor func(T) R) *Collection[R] -``` - -Applies the constructor to each item, returning a new collection. Similar to `Map` but the constructor receives only the item, not the index. - -```go -type DTO struct{ Value string } - -raw := collection.New("a", "b", "c") -dtos := collection.MapInto(raw, func(s string) DTO { - return DTO{Value: strings.ToUpper(s)} -}) -``` - -Use `MapInto` for wrapping raw values into domain types. - ---- - -### Reduce - -```go -func Reduce[T any, R any](c *Collection[T], callback func(R, T, int) R, initial R) R -``` - -Iterates over the collection and accumulates a single result. - -```go -prices := collection.New(19.99, 29.99, 9.99) -total := collection.Reduce(prices, func(sum float64, price float64, _ int) float64 { - return sum + price -}, 0.0) -fmt.Println(total) // 59.97 -``` - -Use `Reduce` for computing aggregates, building maps, or any fold operation. - ---- - -### Flatten - -```go -func (c *Collection[T]) Flatten() *Collection[T] -``` - -Returns a shallow copy of the collection. For typed Go slices (which are inherently flat), this simply copies the items. - -```go -c := collection.New(1, 2, 3) -flat := c.Flatten() -``` - ---- - -### Values - -```go -func (c *Collection[T]) Values() *Collection[T] -``` - -Returns a new collection with items re-indexed (a shallow copy). - -```go -c := collection.New("a", "b", "c") -v := c.Values() -fmt.Println(v.All()) // [a b c] -``` - -Use `Values` after operations that may have changed ordering to get a clean, re-indexed copy. - ---- - -### Reverse - -```go -func (c *Collection[T]) Reverse() *Collection[T] -``` - -Returns a new collection with items in reverse order. - -```go -c := collection.New(1, 2, 3) -fmt.Println(c.Reverse().All()) // [3 2 1] -``` - -Use `Reverse` for displaying results in reverse chronological order or inverting a sorted list. - ---- - -### Shuffle - -```go -func (c *Collection[T]) Shuffle() *Collection[T] -``` - -Returns a new collection with items in random order. - -```go -c := collection.New(1, 2, 3, 4, 5) -shuffled := c.Shuffle() -fmt.Println(shuffled.All()) // random order -``` - -Use `Shuffle` for randomizing quiz questions, playlist order, or A/B test assignments. - ---- - -### Random - -```go -func (c *Collection[T]) Random(counts ...int) *Collection[T] -``` - -Returns a new collection with randomly selected items. Defaults to 1 item if no count is given. - -```go -c := collection.New("a", "b", "c", "d", "e") -sample := c.Random(2) -fmt.Println(sample.Count()) // 2 -``` - -Use `Random` for sampling a subset of data. - ---- - -### Flip - -```go -func (c *Collection[T]) Flip() *Collection[T] -``` - -Returns a new collection with items in reverse order. For typed Go slices, this behaves identically to `Reverse`. - -```go -c := collection.New(1, 2, 3) -fmt.Println(c.Flip().All()) // [3 2 1] -``` - ---- - -## Sorting - -### Sort - -```go -func (c *Collection[T]) Sort(less func(a, b T) bool) *Collection[T] -``` - -Returns a new collection sorted using the provided comparison function. Uses a stable sort. - -```go -c := collection.New(3, 1, 4, 1, 5) -sorted := c.Sort(func(a, b int) bool { return a < b }) -fmt.Println(sorted.All()) // [1 1 3 4 5] -``` - -Use `Sort` when you need full control over the comparison logic. - ---- - -### SortBy - -```go -func SortBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) *Collection[T] -``` - -Returns a new collection sorted in ascending order by the key extracted from each item. - -```go -type Task struct { - Name string - Priority int -} - -tasks := collection.New( - Task{"Deploy", 3}, - Task{"Test", 1}, - Task{"Build", 2}, -) -sorted := collection.SortBy(tasks, func(t Task) int { - return t.Priority -}) -// [{Test 1} {Build 2} {Deploy 3}] -``` - -Use `SortBy` for clean, declarative ascending sorts by a single field. - ---- - -### SortByDesc - -```go -func SortByDesc[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) *Collection[T] -``` - -Returns a new collection sorted in descending order by the extracted key. - -```go -sorted := collection.SortByDesc(tasks, func(t Task) int { - return t.Priority -}) -// [{Deploy 3} {Build 2} {Test 1}] -``` - -Use `SortByDesc` for "highest first" or "most recent first" ordering. - ---- - -### SortDesc - -```go -func (c *Collection[T]) SortDesc(less func(a, b T) bool) *Collection[T] -``` - -Returns a new collection sorted in descending order using the provided comparison function. Reverses the sense of the `less` function. - -```go -c := collection.New(3, 1, 4, 1, 5) -sorted := c.SortDesc(func(a, b int) bool { return a < b }) -fmt.Println(sorted.All()) // [5 4 3 1 1] -``` - -Use `SortDesc` when you need custom descending sort logic. - ---- - -## Partitioning - -### Chunk - -```go -func (c *Collection[T]) Chunk(size int) [][]T -``` - -Breaks the collection into multiple slices of the given size. - -```go -c := collection.New(1, 2, 3, 4, 5) -chunks := c.Chunk(2) -// [[1 2] [3 4] [5]] -``` - -Use `Chunk` for batch processing, pagination, or splitting work across goroutines. - ---- - -### ChunkWhile - -```go -func (c *Collection[T]) ChunkWhile(callback func(T, int, []T) bool) [][]T -``` - -Breaks the collection into groups as long as the callback returns `true`. A new group starts each time the callback returns `false`. The callback receives the current item, its index, and the current chunk. - -```go -c := collection.New(1, 1, 2, 2, 3, 3) -chunks := c.ChunkWhile(func(val int, _ int, current []int) bool { - return val == current[len(current)-1] -}) -// [[1 1] [2 2] [3 3]] -``` - -Use `ChunkWhile` to group consecutive runs of related items. - ---- - -### Split - -```go -func (c *Collection[T]) Split(numberOfGroups int) [][]T -``` - -Splits the collection into the given number of groups, distributing items as evenly as possible. - -```go -c := collection.New(1, 2, 3, 4, 5) -groups := c.Split(3) -// [[1 2] [3 4] [5]] -``` - -Use `Split` for distributing work across a fixed number of workers. - ---- - -### SplitIn - -```go -func (c *Collection[T]) SplitIn(numberOfGroups int) [][]T -``` - -Splits the collection into groups, filling non-terminal groups completely before starting the next. - -```go -c := collection.New(1, 2, 3, 4, 5) -groups := c.SplitIn(3) -// [[1 2] [3 4] [5]] -``` - -Use `SplitIn` when you want consistently full groups with only the last group potentially smaller. - ---- - -### Sliding - -```go -func (c *Collection[T]) Sliding(size int, steps ...int) [][]T -``` - -Returns a sliding window view of the collection. The optional step parameter controls how far the window moves between iterations (default 1). - -```go -c := collection.New(1, 2, 3, 4, 5) -windows := c.Sliding(3) -// [[1 2 3] [2 3 4] [3 4 5]] - -windows = c.Sliding(3, 2) -// [[1 2 3] [3 4 5]] -``` - -Use `Sliding` for moving average calculations, n-gram extraction, or any sliding window analysis. - ---- - -### Partition - -```go -func (c *Collection[T]) Partition(callback func(T, int) bool) (*Collection[T], *Collection[T]) -``` - -Splits the collection into two: items that pass the predicate and items that do not. - -```go -nums := collection.New(1, 2, 3, 4, 5, 6) -evens, odds := nums.Partition(func(n int, _ int) bool { - return n%2 == 0 -}) -fmt.Println(evens.All()) // [2 4 6] -fmt.Println(odds.All()) // [1 3 5] -``` - -Use `Partition` to split data into two categories in a single pass. - ---- - -## Slicing - -### Slice - -```go -func (c *Collection[T]) Slice(offset int, lengths ...int) *Collection[T] -``` - -Extracts a portion of the collection starting at the given offset. Negative offsets count from the end. An optional length limits how many items are returned. - -```go -c := collection.New("a", "b", "c", "d", "e") -fmt.Println(c.Slice(1, 3).All()) // [b c d] -fmt.Println(c.Slice(-2).All()) // [d e] -``` - -Use `Slice` for extracting subsections from a collection. - ---- - -### Splice - -```go -func (c *Collection[T]) Splice(offset int, lengths ...int) *Collection[T] -``` - -Removes and returns a slice of items starting at the given offset. Mutates the original collection. - -```go -c := collection.New(1, 2, 3, 4, 5) -removed := c.Splice(1, 2) -fmt.Println(removed.All()) // [2 3] -fmt.Println(c.All()) // [1 4 5] -``` - -Use `Splice` to extract and remove a segment from the middle of a collection. - ---- - -### SpliceReplace - -```go -func (c *Collection[T]) SpliceReplace(offset, length int, replacement []T) *Collection[T] -``` - -Removes a portion at the given offset and replaces it with the provided items. Returns the removed items. - -```go -c := collection.New("a", "b", "c", "d") -removed := c.SpliceReplace(1, 2, []string{"X", "Y", "Z"}) -fmt.Println(removed.All()) // [b c] -fmt.Println(c.All()) // [a X Y Z d] -``` - -Use `SpliceReplace` for surgical in-place modifications like replacing elements in a pipeline. - ---- - -### Take - -```go -func (c *Collection[T]) Take(limit int) *Collection[T] -``` - -Returns a new collection with the specified number of items from the front. A negative limit takes from the end. - -```go -c := collection.New(1, 2, 3, 4, 5) -fmt.Println(c.Take(3).All()) // [1 2 3] -fmt.Println(c.Take(-2).All()) // [4 5] -``` - -Use `Take` to limit results, such as "top N" queries. - ---- - -### TakeUntil - -```go -func (c *Collection[T]) TakeUntil(callback func(T, int) bool) *Collection[T] -``` - -Returns items from the start until the callback returns `true` (the matching item is excluded). - -```go -c := collection.New(1, 2, 3, 4, 5) -result := c.TakeUntil(func(n int, _ int) bool { return n > 3 }) -fmt.Println(result.All()) // [1 2 3] -``` - -Use `TakeUntil` to collect items up to a boundary condition. - ---- - -### TakeWhile - -```go -func (c *Collection[T]) TakeWhile(callback func(T, int) bool) *Collection[T] -``` - -Returns items from the start as long as the callback returns `true`. Stops at the first `false`. - -```go -c := collection.New(1, 2, 3, 4, 5) -result := c.TakeWhile(func(n int, _ int) bool { return n < 4 }) -fmt.Println(result.All()) // [1 2 3] -``` - -Use `TakeWhile` to collect a leading run of items matching a condition. - ---- - -### Skip - -```go -func (c *Collection[T]) Skip(count int) *Collection[T] -``` - -Returns a new collection with the first `count` items removed. - -```go -c := collection.New(1, 2, 3, 4, 5) -fmt.Println(c.Skip(2).All()) // [3 4 5] -``` - -Use `Skip` for pagination offsets or discarding headers. - ---- - -### SkipUntil - -```go -func (c *Collection[T]) SkipUntil(callback func(T, int) bool) *Collection[T] -``` - -Skips items until the callback returns `true`, then returns the rest (including the matching item). - -```go -c := collection.New(1, 2, 3, 4, 5) -result := c.SkipUntil(func(n int, _ int) bool { return n == 3 }) -fmt.Println(result.All()) // [3 4 5] -``` - -Use `SkipUntil` to start processing from a known marker. - ---- - -### SkipWhile - -```go -func (c *Collection[T]) SkipWhile(callback func(T, int) bool) *Collection[T] -``` - -Skips items while the callback returns `true`, then returns the rest. - -```go -c := collection.New(1, 2, 3, 4, 5) -result := c.SkipWhile(func(n int, _ int) bool { return n < 3 }) -fmt.Println(result.All()) // [3 4 5] -``` - -Use `SkipWhile` to drop a leading run of items matching a condition. - ---- - -### Nth - -```go -func (c *Collection[T]) Nth(step int, offsets ...int) *Collection[T] -``` - -Returns a new collection containing every `step`-th element, starting at an optional offset. - -```go -c := collection.New("a", "b", "c", "d", "e", "f") -fmt.Println(c.Nth(2).All()) // [a c e] -fmt.Println(c.Nth(2, 1).All()) // [b d f] -``` - -Use `Nth` for sampling at regular intervals or selecting alternating elements. - ---- - -### ForPage - -```go -func (c *Collection[T]) ForPage(page, perPage int) *Collection[T] -``` - -Returns a subset of items for the given 1-based page number and page size. - -```go -c := collection.Range(1, 20) -page2 := c.ForPage(2, 5) -fmt.Println(page2.All()) // [6 7 8 9 10] -``` - -Use `ForPage` for pagination. - ---- - -## Combining - -### Concat - -```go -func (c *Collection[T]) Concat(items []T) *Collection[T] -``` - -Returns a new collection with the given items appended. - -```go -a := collection.New(1, 2) -b := a.Concat([]int{3, 4}) -fmt.Println(b.All()) // [1 2 3 4] -``` - -Use `Concat` to join two datasets. - ---- - -### Merge - -```go -func (c *Collection[T]) Merge(items []T) *Collection[T] -``` - -Returns a new collection with the given items appended. Alias for `Concat`. - ---- - -### Pad - -```go -func (c *Collection[T]) Pad(size int, value T) *Collection[T] -``` - -Pads the collection to the specified length with the given value. A positive size pads on the right; a negative size pads on the left. - -```go -c := collection.New(1, 2, 3) -fmt.Println(c.Pad(5, 0).All()) // [1 2 3 0 0] -fmt.Println(c.Pad(-5, 0).All()) // [0 0 1 2 3] -``` - -Use `Pad` to ensure a collection meets a minimum size. - ---- - -### Multiply - -```go -func (c *Collection[T]) Multiply(multiplier int) *Collection[T] -``` - -Returns a new collection with all items repeated the given number of times. - -```go -c := collection.New("a", "b") -fmt.Println(c.Multiply(3).All()) // [a b a b a b] -``` - -Use `Multiply` for repeating patterns or filling templates. - ---- - -### Zip - -```go -func Zip[T any](c *Collection[T], others ...[]T) *Collection[[]T] -``` - -Merges the collection with each of the given slices element-by-element, producing a collection of grouped slices. - -```go -names := collection.New("Alice", "Bob", "Carol") -ages := []string{"30", "25", "35"} -zipped := collection.Zip(names, ages) -// [["Alice" "30"] ["Bob" "25"] ["Carol" "35"]] -``` - -Use `Zip` to combine parallel arrays into rows. - ---- - -### CrossJoin - -```go -func CrossJoin[T any](c *Collection[T], others ...[]T) *Collection[[]T] -``` - -Returns the cross product (Cartesian product) of the collection with the given slices. - -```go -sizes := collection.New("S", "M", "L") -colors := []string{"Red", "Blue"} -combos := collection.CrossJoin(sizes, colors) -// [["S" "Red"] ["S" "Blue"] ["M" "Red"] ["M" "Blue"] ["L" "Red"] ["L" "Blue"]] -``` - -Use `CrossJoin` for generating all possible combinations, such as product variants. - ---- - -### Combine - -```go -func Combine[K any, V any](keys *Collection[K], values []V) *Collection[Pair[K, V]] -``` - -Pairs keys from the collection with values from the given slice, returning a collection of `Pair[K, V]`. - -```go -keys := collection.New("name", "age", "city") -vals := []string{"Alice", "30", "NYC"} -pairs := collection.Combine(keys, vals) -// [{name Alice} {age 30} {city NYC}] -``` - -Use `Combine` to create key-value associations from two parallel lists. - ---- - -### Collapse - -```go -func Collapse[T any](c *Collection[[]T]) *Collection[T] -``` - -Flattens a collection of slices into a single, flat collection. - -```go -nested := collection.New([]int{1, 2}, []int{3, 4}, []int{5}) -flat := collection.Collapse(nested) -fmt.Println(flat.All()) // [1 2 3 4 5] -``` - -Use `Collapse` to flatten one level of nesting. - ---- - -### Diff - -```go -func Diff[T comparable](c *Collection[T], items []T) *Collection[T] -``` - -Returns items in the collection that are not present in the given slice. - -```go -c := collection.New(1, 2, 3, 4, 5) -diff := collection.Diff(c, []int{2, 4}) -fmt.Println(diff.All()) // [1 3 5] -``` - -Use `Diff` to find what is unique to the collection. - ---- - -### DiffUsing - -```go -func (c *Collection[T]) DiffUsing(items []T, equals func(T, T) bool) *Collection[T] -``` - -Like `Diff`, but uses a custom equality function instead of `==`. - -```go -type Item struct{ ID int; Name string } - -existing := collection.New(Item{1, "A"}, Item{2, "B"}, Item{3, "C"}) -toRemove := []Item{{2, "B"}} -remaining := existing.DiffUsing(toRemove, func(a, b Item) bool { - return a.ID == b.ID -}) -fmt.Println(remaining.Count()) // 2 -``` - -Use `DiffUsing` when items are not comparable with `==` or when you need custom matching. - ---- - -### Intersect - -```go -func Intersect[T comparable](c *Collection[T], items []T) *Collection[T] -``` - -Returns items present in both the collection and the given slice. - -```go -c := collection.New(1, 2, 3, 4, 5) -common := collection.Intersect(c, []int{2, 4, 6}) -fmt.Println(common.All()) // [2 4] -``` - -Use `Intersect` to find shared elements between two datasets. - ---- - -### IntersectUsing - -```go -func (c *Collection[T]) IntersectUsing(items []T, equals func(T, T) bool) *Collection[T] -``` - -Like `Intersect`, but uses a custom equality function. - -```go -type Tag struct{ Slug string } - -mine := collection.New(Tag{"go"}, Tag{"rust"}, Tag{"python"}) -theirs := []Tag{{Slug: "go"}, {Slug: "java"}} -shared := mine.IntersectUsing(theirs, func(a, b Tag) bool { - return a.Slug == b.Slug -}) -fmt.Println(shared.Count()) // 1 -``` - -Use `IntersectUsing` for custom struct comparisons. - ---- - -## String - -### Implode - -```go -func (c *Collection[T]) Implode(glue string) string -``` - -Joins all items into a single string separated by the given glue. Each item is converted via `fmt.Sprint`. - -```go -c := collection.New("Go", "is", "great") -fmt.Println(c.Implode(" ")) // "Go is great" -``` - -Use `Implode` for simple string concatenation. - ---- - -### Join - -```go -func (c *Collection[T]) Join(glue string, finalGlues ...string) string -``` - -Joins all items into a string separated by `glue`. An optional final glue is placed between the last two items. - -```go -c := collection.New("Alice", "Bob", "Carol") -fmt.Println(c.Join(", ", ", and ")) -// "Alice, Bob, and Carol" -``` - -Use `Join` for human-readable lists with a natural language connector. - ---- - -## Conditional - -### When - -```go -func (c *Collection[T]) When(condition bool, callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] -``` - -Applies the callback if the condition is `true`. An optional default callback is applied when the condition is `false`. - -```go -isAdmin := true -c := collection.New("read", "write", "delete"). - When(isAdmin, func(c *collection.Collection[string]) *collection.Collection[string] { - return c.Push("admin") - }) -``` - -Use `When` for conditional processing in a fluent chain. - ---- - -### WhenEmpty - -```go -func (c *Collection[T]) WhenEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] -``` - -Applies the callback when the collection is empty. - -```go -c := collection.Empty[string](). - WhenEmpty(func(c *collection.Collection[string]) *collection.Collection[string] { - return c.Push("default value") - }) -fmt.Println(c.All()) // [default value] -``` - -Use `WhenEmpty` to provide fallback data. - ---- - -### WhenNotEmpty - -```go -func (c *Collection[T]) WhenNotEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] -``` - -Applies the callback when the collection is not empty. - -```go -c := collection.New(1, 2, 3). - WhenNotEmpty(func(c *collection.Collection[int]) *collection.Collection[int] { - return c.Push(4) - }) -fmt.Println(c.Count()) // 4 -``` - -Use `WhenNotEmpty` to perform processing only when there is data. - ---- - -### Unless - -```go -func (c *Collection[T]) Unless(condition bool, callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] -``` - -Applies the callback unless the condition is `true`. The inverse of `When`. - -```go -isReadOnly := false -c := collection.New("data"). - Unless(isReadOnly, func(c *collection.Collection[string]) *collection.Collection[string] { - return c.Push("appended") - }) -``` - -Use `Unless` when the inverted condition reads more naturally. - ---- - -### UnlessEmpty - -```go -func (c *Collection[T]) UnlessEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] -``` - -Applies the callback unless the collection is empty. Equivalent to `WhenNotEmpty`. - ---- - -### UnlessNotEmpty - -```go -func (c *Collection[T]) UnlessNotEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] -``` - -Applies the callback unless the collection is not empty. Equivalent to `WhenEmpty`. - ---- - -## Aggregation - -### Sum - -```go -func Sum[T Numeric](c *Collection[T]) T -``` - -Returns the sum of all items. `T` must satisfy the `Numeric` constraint (any integer or float type). - -```go -c := collection.New(10, 20, 30) -fmt.Println(collection.Sum(c)) // 60 -``` - -Use `Sum` for totaling numeric collections. - ---- - -### SumBy - -```go -func SumBy[T any, N Numeric](c *Collection[T], valueFunc func(T) N) N -``` - -Returns the sum of values extracted from each item by the given function. - -```go -type LineItem struct { - Product string - Price float64 -} - -items := collection.New( - LineItem{"Widget", 9.99}, - LineItem{"Gadget", 24.99}, -) -total := collection.SumBy(items, func(li LineItem) float64 { - return li.Price -}) -fmt.Println(total) // 34.98 -``` - -Use `SumBy` to total a specific field from struct collections. - ---- - -### Avg - -```go -func Avg[T Numeric](c *Collection[T]) float64 -``` - -Returns the arithmetic mean of all items. - -```go -c := collection.New(10.0, 20.0, 30.0) -fmt.Println(collection.Avg(c)) // 20 -``` - -Use `Avg` to compute the average of a numeric collection. - ---- - -### AvgBy - -```go -func AvgBy[T any, N Numeric](c *Collection[T], valueFunc func(T) N) float64 -``` - -Returns the arithmetic mean of values extracted by the given function. - -```go -type Score struct { - Student string - Points int -} - -scores := collection.New(Score{"A", 80}, Score{"B", 90}, Score{"C", 70}) -avg := collection.AvgBy(scores, func(s Score) int { return s.Points }) -fmt.Println(avg) // 80 -``` - -Use `AvgBy` to compute averages over a specific field. - ---- - -### Average - -```go -func Average[T Numeric](c *Collection[T]) float64 -``` - -Alias for `Avg`. - ---- - -### Min - -```go -func Min[T cmp.Ordered](c *Collection[T]) (T, bool) -``` - -Returns the minimum value. `T` must satisfy `cmp.Ordered`. The second return value indicates whether the collection was non-empty. - -```go -c := collection.New(5, 3, 8, 1, 4) -min, ok := collection.Min(c) -fmt.Println(min, ok) // 1 true -``` - -Use `Min` to find the smallest value in a comparable collection. - ---- - -### MinBy - -```go -func MinBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) (T, bool) -``` - -Returns the item with the minimum key as determined by the given function. - -```go -type Employee struct { - Name string - Salary float64 -} - -employees := collection.New( - Employee{"Alice", 75000}, - Employee{"Bob", 55000}, - Employee{"Carol", 90000}, -) -lowest, _ := collection.MinBy(employees, func(e Employee) float64 { - return e.Salary -}) -fmt.Println(lowest.Name) // Bob -``` - -Use `MinBy` to find the item with the lowest value in a specific field. - ---- - -### Max - -```go -func Max[T cmp.Ordered](c *Collection[T]) (T, bool) -``` - -Returns the maximum value. The second return value indicates whether the collection was non-empty. - -```go -c := collection.New(5, 3, 8, 1, 4) -max, ok := collection.Max(c) -fmt.Println(max, ok) // 8 true -``` - -Use `Max` to find the largest value in a comparable collection. - ---- - -### MaxBy - -```go -func MaxBy[T any, K cmp.Ordered](c *Collection[T], keyFunc func(T) K) (T, bool) -``` - -Returns the item with the maximum key as determined by the given function. - -```go -highest, _ := collection.MaxBy(employees, func(e Employee) float64 { - return e.Salary -}) -fmt.Println(highest.Name) // Carol -``` - -Use `MaxBy` to find the item with the highest value in a specific field. - ---- - -### Median - -```go -func Median(c *Collection[float64]) float64 -``` - -Returns the median value. Works on `*Collection[float64]`. Returns 0 for an empty collection. - -```go -c := collection.New(1.0, 3.0, 2.0, 5.0, 4.0) -fmt.Println(collection.Median(c)) // 3 -``` - -Use `Median` for statistical analysis where the middle value is more representative than the mean. - ---- - -### MedianBy - -```go -func MedianBy[T any](c *Collection[T], valueFunc func(T) float64) float64 -``` - -Returns the median of values extracted from each item by the given function. - -```go -type Response struct { - LatencyMs float64 -} - -responses := collection.New( - Response{120}, Response{80}, Response{200}, Response{95}, Response{150}, -) -median := collection.MedianBy(responses, func(r Response) float64 { - return r.LatencyMs -}) -fmt.Println(median) // 120 -``` - -Use `MedianBy` for computing median values of struct fields. - ---- - -### Mode - -```go -func Mode[T comparable](c *Collection[T]) []T -``` - -Returns the most frequently occurring values. Returns multiple values in case of a tie. - -```go -c := collection.New("a", "b", "a", "c", "b", "a") -fmt.Println(collection.Mode(c)) // [a] -``` - -Use `Mode` to find the most common elements in a dataset. - ---- - -### CountBy - -```go -func CountBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]int -``` - -Counts how many items produce each key from the given function. - -```go -type LogEntry struct { - Level string - Msg string -} - -logs := collection.New( - LogEntry{"error", "timeout"}, - LogEntry{"info", "started"}, - LogEntry{"error", "crash"}, - LogEntry{"info", "finished"}, -) -counts := collection.CountBy(logs, func(l LogEntry) string { - return l.Level -}) -fmt.Println(counts) // map[error:2 info:2] -``` - -Use `CountBy` for frequency analysis and histograms. - ---- - -### GroupBy - -```go -func GroupBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]*Collection[T] -``` - -Groups items by a key returned from the given function, producing a map of collections. - -```go -type Order struct { - Customer string - Amount float64 -} - -orders := collection.New( - Order{"Alice", 100}, Order{"Bob", 50}, - Order{"Alice", 200}, Order{"Bob", 75}, -) -grouped := collection.GroupBy(orders, func(o Order) string { - return o.Customer -}) -fmt.Println(grouped["Alice"].Count()) // 2 -``` - -Use `GroupBy` to organize items into categories. - ---- - -### KeyBy - -```go -func KeyBy[T any, K comparable](c *Collection[T], keyFunc func(T) K) map[K]T -``` - -Indexes the collection by a key, producing a map. If keys collide, the later value wins. - -```go -type User struct { - ID int - Name string -} - -users := collection.New(User{1, "Alice"}, User{2, "Bob"}) -byID := collection.KeyBy(users, func(u User) int { return u.ID }) -fmt.Println(byID[1].Name) // Alice -``` - -Use `KeyBy` to build lookup maps from collections. - ---- - -### MapToDictionary - -```go -func MapToDictionary[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K][]V -``` - -Maps each item to a key-value pair and groups values by key. Similar to `GroupBy` but extracts a specific value. - -```go -type Sale struct { - Region string - Amount float64 -} - -sales := collection.New( - Sale{"East", 100}, Sale{"West", 200}, - Sale{"East", 150}, Sale{"West", 50}, -) -byRegion := collection.MapToDictionary(sales, func(s Sale) (string, float64) { - return s.Region, s.Amount -}) -fmt.Println(byRegion["East"]) // [100 150] -``` - -Use `MapToDictionary` to group extracted values by a key. - ---- - -### MapToGroups - -```go -func MapToGroups[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K][]V -``` - -Alias for `MapToDictionary`. - ---- - -### MapWithKeys - -```go -func MapWithKeys[T any, K comparable, V any](c *Collection[T], callback func(T) (K, V)) map[K]V -``` - -Maps each item to a key-value pair, returning a flat map. If keys collide, the later value wins. - -```go -type Config struct { - Key string - Value string -} - -configs := collection.New(Config{"host", "localhost"}, Config{"port", "8080"}) -m := collection.MapWithKeys(configs, func(c Config) (string, string) { - return c.Key, c.Value -}) -fmt.Println(m["host"]) // localhost -``` - -Use `MapWithKeys` to convert a collection into a map with custom key-value extraction. - ---- - -### Pluck - -```go -func Pluck[T any, V any](c *Collection[T], valueFunc func(T) V) *Collection[V] -``` - -Extracts a value from each item using the given function, returning a new collection. - -```go -type Article struct { - Title string - Author string -} - -articles := collection.New( - Article{"Go Generics", "Alice"}, - Article{"Iterators", "Bob"}, -) -titles := collection.Pluck(articles, func(a Article) string { - return a.Title -}) -fmt.Println(titles.All()) // [Go Generics Iterators] -``` - -Use `Pluck` to extract a single field from each item in a collection of structs. - ---- - -## Serialization - -### ToSlice - -```go -func (c *Collection[T]) ToSlice() []T -``` - -Returns a copy of the underlying slice. - -```go -c := collection.New(1, 2, 3) -s := c.ToSlice() -s[0] = 99 -fmt.Println(c.All()) // [1 2 3] (original unchanged) -``` - -Use `ToSlice` when you need a defensive copy to avoid shared mutation. - ---- - -### ToJSON - -```go -func (c *Collection[T]) ToJSON() ([]byte, error) -``` - -Serializes the collection to JSON bytes. - -```go -c := collection.New("a", "b", "c") -data, _ := c.ToJSON() -fmt.Println(string(data)) // ["a","b","c"] -``` - -Use `ToJSON` for API responses or data serialization. - ---- - -### ToPrettyJSON - -```go -func (c *Collection[T]) ToPrettyJSON() ([]byte, error) -``` - -Serializes the collection to indented JSON bytes (4-space indent). - -```go -c := collection.New(1, 2, 3) -data, _ := c.ToPrettyJSON() -fmt.Println(string(data)) -// [ -// 1, -// 2, -// 3 -// ] -``` - -Use `ToPrettyJSON` for human-readable output, debugging, or config file generation. - ---- - -### String - -```go -func (c *Collection[T]) String() string -``` - -Returns the JSON string representation of the collection. Returns `"[]"` on encoding error. - -```go -c := collection.New(1, 2, 3) -fmt.Println(c.String()) // [1,2,3] -``` - -Implements `fmt.Stringer` for convenient printing. - ---- - -### MarshalJSON - -```go -func (c *Collection[T]) MarshalJSON() ([]byte, error) -``` - -Implements the `json.Marshaler` interface, allowing collections to be directly marshaled. - -```go -type Response struct { - Items *collection.Collection[string] `json:"items"` -} - -resp := Response{Items: collection.New("a", "b")} -data, _ := json.Marshal(resp) -fmt.Println(string(data)) // {"items":["a","b"]} -``` - ---- - -### UnmarshalJSON - -```go -func (c *Collection[T]) UnmarshalJSON(data []byte) error -``` - -Implements the `json.Unmarshaler` interface, allowing JSON to be decoded directly into a collection. - -```go -var c collection.Collection[int] -json.Unmarshal([]byte("[1,2,3]"), &c) -fmt.Println(c.All()) // [1 2 3] -``` - ---- - -### Copy - -```go -func (c *Collection[T]) Copy() *Collection[T] -``` - -Creates a shallow copy of the collection. - -```go -original := collection.New(1, 2, 3) -clone := original.Copy() -clone.Push(4) -fmt.Println(original.Count()) // 3 (unchanged) -fmt.Println(clone.Count()) // 4 -``` - -Use `Copy` when you need to modify a collection without affecting the original. - ---- - -### Dump - -```go -func (c *Collection[T]) Dump() *Collection[T] -``` - -Prints the collection items to stdout for debugging. Returns the collection for chaining. - -```go -collection.New(1, 2, 3). - Dump(). // prints: [1 2 3] - Filter(func(n int, _ int) bool { return n > 1 }). - Dump() // prints: [2 3] -``` - -Use `Dump` for quick debugging in a fluent chain. - ---- - -### Len - -```go -func (c *Collection[T]) Len() int -``` - -Returns the number of items. Identical to `Count` but satisfies the `sort.Interface` convention. - ---- - -## Conversion - -### Lazy - -```go -func (c *Collection[T]) Lazy() *LazyCollection[T] -``` - -Converts the collection into a `LazyCollection[T]` for deferred evaluation. - -```go -c := collection.New(1, 2, 3, 4, 5) -lazy := c.Lazy() -// No work done until you consume: -result := lazy.Filter(func(n int, _ int) bool { return n > 2 }).All() -fmt.Println(result) // [3 4 5] -``` - -Use `Lazy` when you want to chain multiple operations on a large dataset without creating intermediate slices. - ---- - -### ToBase - -```go -func (c *Collection[T]) ToBase() *Collection[T] -``` - -Returns the collection itself. Provided for API completeness. - ---- - -### Dot - -```go -func (c *Collection[T]) Dot() *Collection[T] -``` - -Returns a shallow copy. For typed Go slices, dot-notation expansion is not applicable. - ---- - -### Undot - -```go -func (c *Collection[T]) Undot() *Collection[T] -``` - -Returns a shallow copy. For typed Go slices, dot-notation expansion is not applicable. - ---- - -### Ensure - -```go -func (c *Collection[T]) Ensure(predicate func(T) bool) error -``` - -Verifies that all items satisfy the predicate. Returns an error if any item fails the check. - -```go -ages := collection.New(18, 25, 16, 30) -err := ages.Ensure(func(age int) bool { return age >= 18 }) -if err != nil { - fmt.Println(err) // "collection item failed ensure check" -} -``` - -Use `Ensure` for validation, such as asserting all items meet a constraint before processing. - ---- - -### Every - -```go -func (c *Collection[T]) Every(callback func(T, int) bool) bool -``` - -Reports whether all items satisfy the predicate. - -```go -c := collection.New(2, 4, 6, 8) -allEven := c.Every(func(n int, _ int) bool { return n%2 == 0 }) -fmt.Println(allEven) // true -``` - -Use `Every` for "all match" checks where you need a boolean result. - ---- - -### Only - -```go -func (c *Collection[T]) Only(indices ...int) *Collection[T] -``` - -Returns a new collection containing only items at the given indices. - -```go -c := collection.New("a", "b", "c", "d", "e") -fmt.Println(c.Only(0, 2, 4).All()) // [a c e] -``` - -Use `Only` to cherry-pick specific positions from a collection. - ---- - -### Except - -```go -func (c *Collection[T]) Except(indices ...int) *Collection[T] -``` - -Returns a new collection excluding items at the given indices. - -```go -c := collection.New("a", "b", "c", "d", "e") -fmt.Println(c.Except(1, 3).All()) // [a c e] -``` - -Use `Except` to remove items at known positions without mutating the original. +πŸ‘‰ [**Back to Overview**](overview.md) diff --git a/docs/helpers.md b/docs/helpers.md index a634095..a07b059 100644 --- a/docs/helpers.md +++ b/docs/helpers.md @@ -1,172 +1,120 @@ -# Helpers +# 🧩 Helper Functions -The root `collection` package includes a small set of general-purpose helper functions. These are standalone utilities that complement the collection types. +The root `collection` package includes a set of general-purpose helper functions. These standalone utilities complement the collection types and simplify common Go patterns. -## Value +--- -Returns the given value unchanged. Useful as a no-op callback or for consistent value resolution patterns. +## πŸ›  Available Helpers + +| Function | Purpose | +|:---|:---| +| [**Value**](#value) | Returns the given value unchanged (no-op). | +| [**ValueFunc**](#valuefunc) | Calls a callback and returns its result (lazy evaluation). | +| [**Head**](#head) | Safely returns the first element of a slice. | +| [**Last**](#last) | Safely returns the last element of a slice. | +| [**WhenValue**](#whenvalue) | Inline conditional for values. | +| [**WhenFunc**](#whenfunc) | Inline conditional for callbacks (lazy). | +| [**Error Types**](#error-types) | Specialized errors for collection operations. | +| [**Shared Types**](#shared-types) | Common type constraints and structures. | + +--- + +## πŸ’Ž Value ```go func Value[T any](value T) T ``` +Returns the given value unchanged. Useful as a no-op callback or for consistent value resolution patterns. + ```go v := collection.Value(42) // 42 ``` -## ValueFunc +--- -Calls the given callback and returns its result. Useful for deferred evaluation. +## πŸ’Ž ValueFunc ```go func ValueFunc[T any](callback func() T) T ``` +Calls the given callback and returns its result. Useful for deferred evaluation. + ```go v := collection.ValueFunc(func() string { return computeExpensiveDefault() }) ``` -**Why:** Allows lazy computation β€” the callback is only executed when ValueFunc is called, not when it's defined. Useful for default values that are expensive to compute. +--- -## Head - -Returns the first element of a slice and `true`, or the zero value and `false` if the slice is empty. +## πŸ’Ž Head ```go func Head[T any](items []T) (T, bool) ``` +Returns the first element of a slice. Returns `false` if the slice is empty. + ```go -first, ok := collection.Head([]string{"alice", "bob", "charlie"}) +first, ok := collection.Head([]string{"alice", "bob"}) // first = "alice", ok = true - -first, ok = collection.Head([]string{}) -// first = "", ok = false ``` -**Why:** A safe way to get the first element without risking a panic on an empty slice. The boolean return lets you distinguish between "empty" and "zero value." - -## Last +--- -Returns the last element of a slice and `true`, or the zero value and `false` if the slice is empty. +## πŸ’Ž Last ```go func Last[T any](items []T) (T, bool) ``` -```go -last, ok := collection.Last([]int{10, 20, 30}) -// last = 30, ok = true -``` - -**Why:** Same safety benefits as Head, but for the tail of the slice. +Returns the last element of a slice. Returns `false` if the slice is empty. -## WhenValue +--- -Returns `value` if `condition` is true, otherwise returns the first default or the zero value. +## πŸ’Ž WhenValue ```go func WhenValue[T any](condition bool, value T, defaults ...T) T ``` +Returns `value` if `condition` is true, otherwise returns the first default or the zero value. + ```go label := collection.WhenValue(isAdmin, "Admin Panel", "Dashboard") -// Returns "Admin Panel" if isAdmin is true, "Dashboard" otherwise - -cssClass := collection.WhenValue(isActive, "active") -// Returns "active" if isActive, "" (zero value) otherwise ``` -**Why:** A concise inline conditional that avoids verbose if/else blocks when selecting between two values. Especially useful when constructing strings, configs, or template data. - -## WhenFunc +--- -Like WhenValue but accepts callbacks for deferred evaluation. Calls the matching callback only when needed. +## πŸ’Ž WhenFunc ```go func WhenFunc[T any](condition bool, callback func() T, defaults ...func() T) T ``` -```go -result := collection.WhenFunc(useCache, - func() []User { return cache.GetUsers() }, - func() []User { return db.QueryUsers() }, -) -``` - -**Why:** Ensures only the needed branch is evaluated. If the cache path is chosen, the database is never queried. Essential when the alternative computations have side effects or are expensive. +Like `WhenValue` but accepts callbacks for deferred evaluation. Only the required branch is executed. -## Error Types +--- -The package defines two error types for collection operations that can fail: +## πŸ›‘ Error Types ### ItemNotFoundError - -Returned by `FirstOrFail`, `Sole`, and similar methods when no matching item exists. - -```go -type ItemNotFoundError struct { - Message string -} -``` - -```go -user, err := users.FirstOrFail(func(u User, _ int) bool { - return u.Email == "missing@example.com" -}) -if err != nil { - // err is *collection.ItemNotFoundError - var notFound *collection.ItemNotFoundError - if errors.As(err, ¬Found) { - log.Println("user not found") - } -} -``` +Returned when a requested item is missing (e.g., `FirstOrFail`, `Sole`). ### MultipleItemsFoundError - Returned by `Sole` when more than one item matches the predicate. -```go -type MultipleItemsFoundError struct { - Count int - Message string -} -``` +--- -```go -admin, err := users.Sole(func(u User, _ int) bool { - return u.Role == "admin" -}) -if err != nil { - var multiple *collection.MultipleItemsFoundError - if errors.As(err, &multiple) { - log.Printf("expected 1 admin, found %d", multiple.Count) - } -} -``` - -**Why:** Typed errors let you use `errors.As` for precise error handling, and the `Count` field on `MultipleItemsFoundError` gives you diagnostic information without parsing error strings. - -## Shared Types +## 🧩 Shared Types ### Numeric +A type constraint for numeric types, used by aggregation functions like `Sum` and `Avg`. -A type constraint for numeric types, used by aggregation functions like `Sum`, `Avg`, `Min`, `Max`. - -```go -type Numeric interface { - ~int | ~int8 | ~int16 | ~int32 | ~int64 | - ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | - ~float32 | ~float64 -} -``` - -### Pair - -A generic key-value pair, used by `Combine`, `NewMapFromPairs`, and `ToPairs`. +### Pair[K, V] +A generic key-value pair used by `Combine` and `MapCollection`. ```go type Pair[K any, V any] struct { @@ -175,10 +123,6 @@ type Pair[K any, V any] struct { } ``` -```go -pairs := []collection.Pair[string, int]{ - {Key: "alice", Value: 95}, - {Key: "bob", Value: 87}, -} -m := collection.NewMapFromPairs(pairs...) -``` +--- + +πŸ‘‰ [**Back to Overview**](overview.md) diff --git a/docs/kv.md b/docs/kv.md index c2b870a..5effba3 100644 --- a/docs/kv.md +++ b/docs/kv.md @@ -1,622 +1,112 @@ -# kv -- Map Utilities with Dot-Notation Support +# 🧩 Key-Value Utilities (`kv`) -``` -import "github.com/gocanto/collection/kv" -``` - -The `kv` package provides standalone utility functions for working with Go maps. Its headline feature is **dot-notation access** for nested `map[string]any` structures (the kind you get from JSON decoding), but it also includes general-purpose filtering, sorting, transformation, and encoding helpers. +`import "github.com/gocanto/collection/kv"` -Use `kv` when you need quick map operations without constructing a `collection.MapCollection`. +The `kv` package provides standalone utility functions for working with Go maps. Its headline feature is **dot-notation access** for nested structures, along with general-purpose filtering, sorting, and transformation helpers. --- -## Table of Contents - -**Dot-notation helpers (nested `map[string]any`)** -- [Get](#get) -- [Set](#set) -- [Has](#has) -- [Fill](#fill) -- [Add](#add) -- [Forget](#forget) -- [ForgetMany](#forgetmany) -- [Pull](#pull) -- [HasAll](#hasall) -- [HasAny](#hasany) -- [Dot](#dot) -- [Undot](#undot) - -**Map filtering and selection** -- [Only](#only) -- [Except](#except) -- [IsAssoc](#isassoc) +## πŸ›  Available Functions -**Encoding** -- [Query](#query) -- [ToCssClasses](#tocssclasses) -- [ToCssStyles](#tocssstyles) +### Dot-Notation Helpers +| Function | Purpose | +|:---|:---| +| [**Get**](#get) | Retrieves a value from a nested map using dot-separated paths. | +| [**Set**](#set) | Sets a value in a nested map using a dot-separated path. | +| [**Has**](#has) | Reports whether a dot-notated key exists. | +| [**Fill**](#fill) | Sets a value only if the key does not exist. | +| [**Add**](#add) | Alias for `Fill`. | +| [**Forget**](#forget) | Removes a value at a given dot-notated key. | +| [**Pull**](#pull) | Retrieves and then removes a value. | +| [**Dot**](#dot) | Flattens a nested map into a single-level map. | +| [**Undot**](#undot) | Expands a flat map back into a nested structure. | -**Sorting** -- [Sort](#sort) -- [SortRecursive](#sortrecursive) +### Map Selection & Transformation +| Function | Purpose | +|:---|:---| +| [**Only**](#only) | Returns a new map containing only the specified keys. | +| [**Except**](#except) | Returns a new map with the specified keys removed. | +| [**Map**](#map) | Transforms map values while preserving keys. | +| [**Where**](#where) | Filters map entries by a custom predicate. | +| [**Replace**](#replace) | Merges maps, with later maps taking precedence. | +| [**Sort**](#sort) | Returns a new map with keys in ascending order. | -**Map transformation** -- [Map](#map) -- [Where](#where) -- [PrependKeysWith](#prependkeyswith) -- [Replace](#replace) +### Encoding +| Function | Purpose | +|:---|:---| +| [**Query**](#query) | Encodes a map as a URL query string. | +| [**ToCssClasses**](#tocssclasses) | Generates a CSS class list from a boolean map. | +| [**ToCssStyles**](#tocssstyles) | Generates inline CSS styles from a boolean map. | --- -## Dot-Notation Helpers +## πŸ’Ž Dot-Notation Helpers -These functions navigate nested `map[string]any` structures using dot-separated key paths such as `"user.address.city"`. They are particularly useful when working with decoded JSON, YAML, or configuration data. - ---- +These functions navigate nested `map[string]any` structures using paths such as `"user.address.city"`. ### Get - ```go func Get(target map[string]any, key string, defaults ...any) any ``` - -Retrieves a value from a nested map using a dot-separated key path. If the key is not found, the first default value is returned (or `nil` if no default is provided). Passing an empty key returns the entire map. - -```go -data := map[string]any{ - "user": map[string]any{ - "name": "Alice", - "address": map[string]any{ - "city": "Portland", - }, - }, -} - -kv.Get(data, "user.name") // "Alice" -kv.Get(data, "user.address.city") // "Portland" -kv.Get(data, "user.phone") // nil -kv.Get(data, "user.phone", "555-0000") // "555-0000" -kv.Get(data, "") // returns the entire map -``` - -**Why it is useful:** Eliminates chains of type assertions and existence checks when reading deeply nested configuration or JSON data. - ---- +Retrieves a nested value. Returns `nil` or a default if not found. ### Set - ```go func Set(target map[string]any, key string, value any, overwrite ...bool) map[string]any ``` - -Sets a value in a nested map using a dot-separated key path, creating intermediate maps as needed. By default, existing values are overwritten. Pass `false` as the last argument to preserve existing values. - -```go -data := map[string]any{} - -kv.Set(data, "app.name", "MyApp") -// data == {"app": {"name": "MyApp"}} - -kv.Set(data, "app.version", "1.0") -// data == {"app": {"name": "MyApp", "version": "1.0"}} - -// With overwrite disabled: -kv.Set(data, "app.name", "OtherApp", false) -// data["app"]["name"] is still "MyApp" -``` - -**Why it is useful:** Builds nested map structures programmatically without manually constructing each intermediate map. - ---- +Sets a nested value, creating intermediate maps as needed. ### Has - ```go func Has(target map[string]any, key string) bool ``` - -Reports whether the given dot-notated key exists in the nested map. Returns `false` for an empty key. - -```go -data := map[string]any{ - "database": map[string]any{ - "host": "localhost", - }, -} - -kv.Has(data, "database.host") // true -kv.Has(data, "database.port") // false -kv.Has(data, "cache.driver") // false -``` - -**Why it is useful:** Checks for the existence of deeply nested keys in a single call. - ---- - -### Fill - -```go -func Fill(target map[string]any, key string, value any) map[string]any -``` - -Sets the value at the given dot-notated key only if that key does not already exist. Existing values are preserved. - -```go -data := map[string]any{ - "app": map[string]any{"name": "MyApp"}, -} - -kv.Fill(data, "app.name", "Other") // no change -- key exists -kv.Fill(data, "app.debug", true) // sets app.debug to true -``` - -**Why it is useful:** Provides safe default-value injection without accidentally overwriting user-provided configuration. - ---- - -### Add - -```go -func Add(target map[string]any, key string, value any) map[string]any -``` - -An alias for `Fill`. Sets a value only if the key does not already exist. - -```go -kv.Add(data, "app.env", "production") -``` - -**Why it is useful:** Offers an alternative name that reads more naturally in certain contexts ("add this key if missing"). - ---- +Reports if a nested key exists. ### Forget - ```go func Forget(target map[string]any, key string) map[string]any ``` - -Removes the value at the given dot-notated key from the nested map. If the key does not exist, the map is returned unchanged. - -```go -data := map[string]any{ - "user": map[string]any{ - "name": "Alice", - "email": "alice@example.com", - }, -} - -kv.Forget(data, "user.email") -// data == {"user": {"name": "Alice"}} -``` - -**Why it is useful:** Deletes nested keys without manually traversing the map hierarchy. - ---- - -### ForgetMany - -```go -func ForgetMany(items map[string]any, keys ...string) map[string]any -``` - -Removes one or more dot-notated keys from the map. - -```go -data := map[string]any{ - "a": 1, "b": 2, "c": 3, "d": 4, -} - -kv.ForgetMany(data, "b", "d") -// data == {"a": 1, "c": 3} -``` - -**Why it is useful:** Batch-deletes multiple keys in a single call. - ---- - -### Pull - -```go -func Pull(items map[string]any, key string, defaults ...any) any -``` - -Retrieves the value at the given dot-notated key and then removes it from the map. If the key does not exist, the first default value is returned. - -```go -data := map[string]any{"token": "abc123", "user": "alice"} - -token := kv.Pull(data, "token") -// token == "abc123" -// data == {"user": "alice"} - -missing := kv.Pull(data, "nope", "default_val") -// missing == "default_val" -``` - -**Why it is useful:** Combines a "get" and a "delete" into one atomic operation, common when consuming one-time values like tokens or nonces. - ---- - -### HasAll - -```go -func HasAll(items map[string]any, keys ...string) bool -``` - -Reports whether **all** of the given dot-notated keys exist in the map. - -```go -data := map[string]any{ - "host": "localhost", - "port": 5432, -} - -kv.HasAll(data, "host", "port") // true -kv.HasAll(data, "host", "port", "user") // false -``` - -**Why it is useful:** Validates that all required configuration keys are present before proceeding. - ---- - -### HasAny - -```go -func HasAny(items map[string]any, keys ...string) bool -``` - -Reports whether **at least one** of the given dot-notated keys exists in the map. - -```go -kv.HasAny(data, "host", "missing_key") // true -kv.HasAny(data, "x", "y", "z") // false -``` - -**Why it is useful:** Checks whether any of several alternative keys are available. - ---- - -### Dot - -```go -func Dot(items map[string]any, prepend ...string) map[string]any -``` - -Flattens a nested map into a single-level map with dot-notated keys. An optional prefix string is prepended to every key. - -```go -nested := map[string]any{ - "user": map[string]any{ - "name": "Alice", - "address": map[string]any{ - "city": "Portland", - "state": "OR", - }, - }, -} - -flat := kv.Dot(nested) -// map[ -// "user.name": "Alice", -// "user.address.city": "Portland", -// "user.address.state": "OR", -// ] - -prefixed := kv.Dot(nested, "config") -// map[ -// "config.user.name": "Alice", -// "config.user.address.city": "Portland", -// "config.user.address.state": "OR", -// ] -``` - -**Why it is useful:** Converts hierarchical data into a flat key-value representation, useful for logging, diffing, or storing as environment variables. - ---- - -### Undot - -```go -func Undot(items map[string]any) map[string]any -``` - -Expands a flat map with dot-notated keys back into a nested map structure. - -```go -flat := map[string]any{ - "database.host": "localhost", - "database.port": 5432, - "app.name": "MyApp", -} - -nested := kv.Undot(flat) -// map[ -// "database": map["host": "localhost", "port": 5432], -// "app": map["name": "MyApp"], -// ] -``` - -**Why it is useful:** The inverse of `Dot` -- reconstructs nested structures from flat key-value stores (environment variables, flat config files). +Removes a nested key. --- -## Map Filtering and Selection - ---- - -### Only +## πŸ’Ž Transformation & Selection +### Only / Except ```go func Only(items map[string]any, keys ...string) map[string]any -``` - -Returns a new map containing only the specified keys. Keys that do not exist in the source are silently skipped. - -```go -data := map[string]any{"a": 1, "b": 2, "c": 3, "d": 4} - -kv.Only(data, "a", "c") -// map["a": 1, "c": 3] -``` - -**Why it is useful:** Picks specific keys from a map to create a subset, commonly used to whitelist fields before serialization. - ---- - -### Except - -```go func Except(items map[string]any, keys ...string) map[string]any ``` +Whitelists or blacklists specific keys. -Returns a new map with the specified keys removed. - -```go -data := map[string]any{"a": 1, "b": 2, "c": 3, "d": 4} - -kv.Except(data, "b", "d") -// map["a": 1, "c": 3] -``` - -**Why it is useful:** The inverse of `Only` -- blacklists keys before passing a map to an API or template. - ---- - -### IsAssoc - +### Map ```go -func IsAssoc(items map[string]any) bool +func Map[V any, R any](items map[string]V, callback func(V, string) R) map[string]R ``` +Transforms values using generics. -Reports whether the map is non-empty. - +### Sort ```go -kv.IsAssoc(map[string]any{"key": "val"}) // true -kv.IsAssoc(map[string]any{}) // false +func Sort(items map[string]any) map[string]any ``` - -**Why it is useful:** A quick check to determine whether a map carries any data before processing it. +Returns a new map with keys sorted alphabetically. --- -## Encoding - ---- +## πŸ’Ž Encoding ### Query - ```go func Query(items map[string]any) string ``` - -Encodes the map as a URL query string. Values are converted to strings via `fmt.Sprint`. - -```go -params := map[string]any{ - "page": 2, - "per_page": 25, - "sort": "name", -} - -kv.Query(params) -// "page=2&per_page=25&sort=name" (keys are sorted alphabetically) -``` - -**Why it is useful:** Builds URL query strings from arbitrary maps without manually constructing `url.Values`. - ---- +Converts a map to a URL-encoded query string. ### ToCssClasses - ```go func ToCssClasses(classes map[string]bool) string ``` - -Returns a space-separated string of CSS class names whose boolean values are `true`. Classes are sorted alphabetically. - -```go -kv.ToCssClasses(map[string]bool{ - "btn": true, - "btn-primary": true, - "disabled": false, - "active": true, -}) -// "active btn btn-primary" -``` - -**Why it is useful:** Conditionally assembles CSS class lists in server-side template rendering. - ---- - -### ToCssStyles - -```go -func ToCssStyles(styles map[string]bool) string -``` - -Returns a space-separated string of CSS style declarations whose boolean values are `true`. A trailing semicolon is appended to each style if not already present. Styles are sorted alphabetically. - -```go -kv.ToCssStyles(map[string]bool{ - "color: red": true, - "font-weight: bold": true, - "display: none": false, -}) -// "color: red; font-weight: bold;" -``` - -**Why it is useful:** Conditionally builds inline CSS style attributes for server-side HTML generation. - ---- - -## Sorting - ---- - -### Sort - -```go -func Sort(items map[string]any) map[string]any -``` - -Returns a new map constructed by iterating over the keys in sorted (ascending) order. - -> **Note:** Go maps do not guarantee iteration order. The returned map is sorted at construction time, but subsequent iteration may not preserve the order. - -```go -data := map[string]any{"c": 3, "a": 1, "b": 2} -sorted := kv.Sort(data) -// Constructed in order: a=1, b=2, c=3 -``` - -**Why it is useful:** Produces deterministic key ordering for logging, serialization, or display purposes. - ---- - -### SortRecursive - -```go -func SortRecursive(items map[string]any) map[string]any -``` - -Like `Sort`, but also recursively sorts any nested `map[string]any` values. - -```go -data := map[string]any{ - "z": map[string]any{"b": 2, "a": 1}, - "a": "first", -} -sorted := kv.SortRecursive(data) -// Outer keys sorted: a, z -// Inner map {"b": 2, "a": 1} also sorted: a, b -``` - -**Why it is useful:** Produces fully deterministic output from deeply nested maps, ideal for consistent hashing or snapshot testing. +Builds a CSS class string from active (true) boolean flags. --- -## Map Transformation - ---- - -### Map - -```go -func Map[V any, R any](items map[string]V, callback func(V, string) R) map[string]R -``` - -Applies the callback to each value in the map and returns a new map with the transformed values. The callback receives the value and its key. - -```go -prices := map[string]float64{ - "apple": 1.50, - "banana": 0.75, -} - -doubled := kv.Map(prices, func(price float64, _ string) float64 { - return price * 2 -}) -// map["apple": 3.0, "banana": 1.5] - -labels := kv.Map(prices, func(price float64, fruit string) string { - return fmt.Sprintf("%s: $%.2f", fruit, price) -}) -// map["apple": "apple: $1.50", "banana": "banana: $0.75"] -``` - -**Why it is useful:** Transforms map values while preserving keys, enabling type-changing transformations via generics. - ---- - -### Where - -```go -func Where[V any](items map[string]V, callback func(V, string) bool) map[string]V -``` - -Returns a new map containing only the entries for which the callback returns `true`. - -```go -scores := map[string]int{ - "alice": 92, - "bob": 67, - "carol": 85, -} - -passing := kv.Where(scores, func(score int, _ string) bool { - return score >= 80 -}) -// map["alice": 92, "carol": 85] -``` - -**Why it is useful:** Filters map entries by value (or key) without constructing a new map manually. - ---- - -### PrependKeysWith - -```go -func PrependKeysWith[V any](items map[string]V, prefix string) map[string]V -``` - -Returns a new map with the given prefix prepended to every key. - -```go -env := map[string]string{ - "HOST": "localhost", - "PORT": "8080", -} - -kv.PrependKeysWith(env, "APP_") -// map["APP_HOST": "localhost", "APP_PORT": "8080"] -``` - -**Why it is useful:** Namespaces map keys in bulk, common when merging configuration from multiple sources. - ---- - -### Replace - -```go -func Replace(items map[string]any, replacements ...map[string]any) map[string]any -``` - -Returns a new map starting with all entries from `items`, then overwrites (or adds) entries from each replacement map in order. - -```go -defaults := map[string]any{ - "timeout": 30, - "retries": 3, - "debug": false, -} -overrides := map[string]any{ - "timeout": 60, - "debug": true, -} - -config := kv.Replace(defaults, overrides) -// map["timeout": 60, "retries": 3, "debug": true] -``` - -**Why it is useful:** Merges configuration layers (defaults, environment, user overrides) with later maps taking precedence. +πŸ‘‰ [**Back to Overview**](overview.md) diff --git a/docs/lazy.md b/docs/lazy.md new file mode 100644 index 0000000..a89fd17 --- /dev/null +++ b/docs/lazy.md @@ -0,0 +1,136 @@ +# 🐒 Lazy API Reference + +The `lazy` package provides a `Collection[T]` type that leverages Go's iterators (`iter.Seq[T]`) for lazy evaluation. Items are only computed when needed, making it highly efficient for processing large datasets or infinite sequences. + +```go +import "github.com/gocanto/collection/lazy" +``` + +### πŸš€ Constructors + +| Function | Description | +| --- | --- | +| `New(source iter.Seq[T])` | Creates a new Collection from an iterator function. | +| `From(slice)` | Creates a Collection from a slice. | +| `Empty()` | Creates an empty Collection. | +| `Range(from, to)` | Creates a collection of sequential integers. | +| `Times(n, callback)` | Creates a collection by invoking the callback `n` times. | + +### πŸ” Query & Access + +| Method / Function | Description | +| --- | --- | +| `Iter()` | Returns the underlying iterator (`iter.Seq[T]`). | +| `All()` | Eagerly evaluates the collection and returns a slice. | +| `Eager()` | Alias for `All`. | +| `Collect()` | Alias for `All`. | +| `Count()` | Returns the total number of items (requires eager evaluation). | +| `IsEmpty()` | Reports whether the collection is empty. | +| `IsNotEmpty()` | Reports whether the collection is not empty. | +| `ContainsOneItem()` | Reports whether the collection has exactly one item. | +| `ContainsManyItems()` | Reports whether the collection has more than one item. | +| `First(predicates...)` | Returns the first item matching the optional predicate. | +| `FirstOrFail(predicates...)` | Returns the first item or an error if not found. | +| `Last(predicates...)` | Returns the last item matching the optional predicate. | +| `Sole(predicates...)` | Returns the only item matching the predicate (error if > 1). | +| `Get(index)` | Returns the item at the given zero-based index. | +| `Has(index)` | Reports whether the given index exists. | +| `HasAny(indices...)` | Reports whether any of the given indices exist. | +| `Contains(predicate)` | Reports whether any item satisfies the predicate. | +| `Some(predicate)` | Alias for `Contains`. | +| `DoesntContain(predicate)` | Reports whether no item satisfies the predicate. | +| `HasSole(predicate)` | Reports whether exactly one item satisfies the predicate. | +| `Search(predicate)` | Returns the index of the first item satisfying the predicate. | +| `Before(predicate)` | Returns the item before the first match. | +| `After(predicate)` | Returns the item after the first match. | +| `Every(predicate)` | Reports whether all items satisfy the predicate. | + +### ♻️ Transformation + +| Method / Function | Description | +| --- | --- | +| `Map(col, callback)` | (Function) Transforms each item using the callback. | +| `FlatMap(col, callback)` | (Function) Transforms and flattens results. | +| `Filter(predicate)` | Returns items satisfying the predicate. | +| `Reject(predicate)` | Returns items NOT satisfying the predicate. | +| `Unique(col, keyFunc)` | (Function) Returns unique items by key. | +| `Pluck(col, valueFunc)` | (Function) Extracts a value from each item. | +| `Reduce(col, callback, init)` | (Function) Reduces the collection to a single value. | +| `GroupBy(col, keyFunc)` | (Function) Groups items by the key. | +| `KeyBy(col, keyFunc)` | (Function) Indexes items by the key. | +| `CountBy(col, keyFunc)` | (Function) Counts occurrences by key. | + +### 🎯 Pipeline & Utilities + +| Method / Function | Description | +| --- | --- | +| `Take(limit)` | Limits the number of items. | +| `TakeUntil(predicate)` | Takes items until the predicate returns true. | +| `TakeWhile(predicate)` | Takes items while the predicate returns true. | +| `Skip(count)` | Skips the first `count` items. | +| `SkipUntil(predicate)` | Skips items until the predicate returns true. | +| `SkipWhile(predicate)` | Skips items while the predicate returns true. | +| `Slice(offset, lengths...)` | Returns a subset of the collection. | +| `Chunk(size)` | Eagerly splits items into groups of the given size. | +| `ChunkWhile(predicate)` | Splits items into groups based on consecutive matches. | +| `Nth(step, offset)` | Returns every step-th element. | +| `Concat(slice)` | Appends items from a slice. | +| `Pad(size, value)` | Pads the collection to a given size. | +| `Each(callback)` | Iterates over items. | +| `Tap(callback)` | Passes the collection to a callback. | +| `TapEach(callback)` | Calls the callback on each item as it passes through. | +| `Throttle(delay)` | Inserts a delay between each yielded item. | +| `Remember()` | Caches items after the first iteration. | +| `Implode(glue)` | Joins items into a string. | +| `Join(glue, finalGlue)` | Joins items with a separator and optional final separator. | +| `When(cond, callback, default)` | Conditional application. | +| `WhenEmpty(callback)` | Applies if the collection is empty. | +| `WhenNotEmpty(callback)` | Applies if the collection is not empty. | +| `Unless(cond, callback)` | Inverse of `When`. | +| `Dump()` | Prints evaluated items for debugging. | + +--- + +## Detailed Methods + +### `Map` +Transforms each item in the collection. This is a package-level function to allow changing the type of the collection. + +```go +numbers := lazy.From([]int{1, 2, 3}) +doubled := lazy.Map(numbers, func(v int, i int) int { + return v * 2 +}) +``` + +### `Take` & `Skip` +Lazy collections can be sliced without evaluating the entire source. + +```go +// Only takes the first 5 even numbers +evens := lazy.Range(1, 100). + Filter(func(v int, _ int) bool { return v%2 == 0 }). + Take(5) +``` + +### `Remember` +By default, lazy collections re-run their logic every time they are iterated. `Remember` caches the results in memory after the first pass. + +```go +cached := lazy.New(expensiveSource).Remember() +cached.All() // Computes +cached.All() // Uses cache +``` + +### `Throttle` +`Throttle` is useful for rate-limiting operations like API calls or log processing. + +```go +// Yields one item every 500ms +lazy.From(items).Throttle(500 * time.Millisecond).Each(func(v T, _ int) bool { + process(v) + return true +}) +``` + +πŸ‘‰ [**Back to Overview**](overview.md) diff --git a/docs/lazy_collection.md b/docs/lazy_collection.md deleted file mode 100644 index 1fa0ab3..0000000 --- a/docs/lazy_collection.md +++ /dev/null @@ -1,1305 +0,0 @@ -# LazyCollection[T] - -`LazyCollection` is a lazily-evaluated sequence backed by `iter.Seq[T]`. Items are computed on demand -- nothing runs until you consume the collection. This makes it ideal for large datasets, pipelines with early termination, and infinite or generated sequences. - -Because operations like `Filter`, `Take`, and `Skip` return new lazy collections rather than materializing results, you can compose long pipelines that only process the elements actually needed. Memory usage stays proportional to what you consume, not to the total dataset size. - -```go -import "github.com/gocanto/collection/collection" -``` - ---- - -## Table of Contents - -- [Constructors](#constructors) -- [Materialization](#materialization) -- [Query](#query) -- [Retrieval](#retrieval) -- [Search](#search) -- [Iteration](#iteration) -- [Filtering](#filtering) -- [Transformation](#transformation) -- [Partitioning](#partitioning) -- [Slicing](#slicing) -- [Combining](#combining) -- [Grouping](#grouping) -- [Caching](#caching) -- [String](#string) -- [Conditional](#conditional) -- [Other](#other) - ---- - -## Why Lazy? - -Consider processing a million-row CSV. With an eager collection you would load all rows into memory, filter them, then transform them. With a lazy collection, each row flows through the pipeline one at a time -- rows that fail the filter never reach the transform step, and if you only need the first 10 matches, the remaining rows are never even read. - -```go -// Only processes rows until 10 matches are found. -// Rows that don't match "active" are never transformed. -results := collection.LazyFrom(millionRows). - Filter(func(r Row, _ int) bool { return r.Status == "active" }). - Take(10). - All() -``` - ---- - -## Constructors - -### NewLazy - -```go -func NewLazy[T any](source iter.Seq[T]) *LazyCollection[T] -``` - -Creates a `LazyCollection` from an `iter.Seq[T]` iterator function. The source receives a `yield` function; call `yield` for each item and stop generating if `yield` returns `false`. - -```go -// A lazy sequence of squares -squares := collection.NewLazy(func(yield func(int) bool) { - for i := 1; i <= 1000; i++ { - if !yield(i * i) { - return - } - } -}) - -first3 := squares.Take(3).All() // [1, 4, 9] -``` - -**When to use:** You have a custom generator function or an existing `iter.Seq[T]` from another library. - ---- - -### LazyFrom - -```go -func LazyFrom[T any](items []T) *LazyCollection[T] -``` - -Creates a `LazyCollection` from a slice. The slice is iterated lazily -- no copy is made up front. - -```go -names := collection.LazyFrom([]string{"Alice", "Bob", "Charlie"}) -first, _ := names.First() // "Alice" -``` - -**When to use:** You have data in a slice but want to apply lazy pipeline operations to avoid intermediate allocations. - ---- - -### LazyEmpty - -```go -func LazyEmpty[T any]() *LazyCollection[T] -``` - -Creates an empty `LazyCollection`. Useful as a starting point or default value. - -```go -empty := collection.LazyEmpty[int]() -fmt.Println(empty.IsEmpty()) // true -``` - ---- - -### LazyRange - -```go -func LazyRange(from, to int) *LazyCollection[int] -``` - -Creates a lazy collection of sequential integers from `from` to `to` (inclusive). If `from > to`, the sequence counts downward. - -```go -ascending := collection.LazyRange(1, 5).All() // [1, 2, 3, 4, 5] -descending := collection.LazyRange(5, 1).All() // [5, 4, 3, 2, 1] -``` - -**When to use:** Generating index ranges, pagination offsets, or numeric test data without allocating a slice. - ---- - -### LazyTimes - -```go -func LazyTimes[T any](number int, callback func(int) T) *LazyCollection[T] -``` - -Creates a lazy collection by invoking the callback `n` times. The callback receives a **1-based** index. - -```go -labels := collection.LazyTimes(3, func(i int) string { - return fmt.Sprintf("item-%d", i) -}) -fmt.Println(labels.All()) // ["item-1", "item-2", "item-3"] -``` - -**When to use:** Creating test fixtures, placeholder data, or repeated computed values. - ---- - -## Materialization - -Materialization methods force evaluation of the lazy pipeline and produce concrete results. - -### All - -```go -func (lc *LazyCollection[T]) All() []T -``` - -Eagerly evaluates the lazy collection and returns all items as a slice. - -```go -items := collection.LazyRange(1, 5).All() // [1, 2, 3, 4, 5] -``` - ---- - -### Eager - -```go -func (lc *LazyCollection[T]) Eager() *Collection[T] -``` - -Eagerly evaluates the lazy collection and returns a `Collection[T]` -- the eager, feature-rich slice wrapper. - -```go -c := collection.LazyFrom([]int{3, 1, 2}).Eager() -// c is now a *Collection[int] with sorting, mapping, etc. -``` - -**When to use:** You have finished building a lazy pipeline and need the full method set of `Collection` for subsequent operations. - ---- - -### Collect - -```go -func (lc *LazyCollection[T]) Collect() *Collection[T] -``` - -Alias for `Eager`. Converts the lazy collection to an eager `Collection`. - ---- - -### Iter - -```go -func (lc *LazyCollection[T]) Iter() iter.Seq[T] -``` - -Returns the underlying `iter.Seq[T]` iterator for use with Go 1.23+ range-over-func loops. - -```go -lazy := collection.LazyFrom([]string{"a", "b", "c"}) - -for item := range lazy.Iter() { - fmt.Println(item) -} -// Output: -// a -// b -// c -``` - -**When to use:** Integrating a lazy pipeline with standard Go `for range` syntax. Because `Iter()` returns an `iter.Seq[T]`, you get first-class compatibility with the Go 1.23+ iterator protocol. Consumers can break out of the loop early and the upstream pipeline stops immediately -- no wasted work. - -```go -// Processing stops as soon as we find what we need. -for val := range collection.LazyRange(1, 1_000_000).Iter() { - if val > 10 { - break // only 11 items were generated - } -} -``` - ---- - -## Query - -### Count - -```go -func (lc *LazyCollection[T]) Count() int -``` - -Returns the total number of items by eagerly evaluating the entire collection. - -```go -count := collection.LazyRange(1, 100).Count() // 100 -``` - -> **Note:** This consumes the full sequence. For single-pass iterators, subsequent calls re-evaluate from the source. - ---- - -### IsEmpty - -```go -func (lc *LazyCollection[T]) IsEmpty() bool -``` - -Reports whether the lazy collection contains no items. Only evaluates the first element to determine emptiness. - -```go -fmt.Println(collection.LazyEmpty[int]().IsEmpty()) // true -fmt.Println(collection.LazyRange(1, 5).IsEmpty()) // false -``` - ---- - -### IsNotEmpty - -```go -func (lc *LazyCollection[T]) IsNotEmpty() bool -``` - -Reports whether the lazy collection contains at least one item. - ---- - -### ContainsOneItem - -```go -func (lc *LazyCollection[T]) ContainsOneItem() bool -``` - -Reports whether the lazy collection contains exactly one item. - -```go -fmt.Println(collection.LazyFrom([]int{42}).ContainsOneItem()) // true -``` - ---- - -### ContainsManyItems - -```go -func (lc *LazyCollection[T]) ContainsManyItems() bool -``` - -Reports whether the lazy collection contains more than one item. - ---- - -### Has - -```go -func (lc *LazyCollection[T]) Has(index int) bool -``` - -Reports whether the given zero-based index exists in the collection. Iterates up to that index. - -```go -lazy := collection.LazyFrom([]string{"a", "b", "c"}) -fmt.Println(lazy.Has(2)) // true -fmt.Println(lazy.Has(10)) // false -``` - ---- - -### HasAny - -```go -func (lc *LazyCollection[T]) HasAny(indices ...int) bool -``` - -Reports whether any of the given indices exist in the collection. - -```go -lazy := collection.LazyFrom([]int{10, 20, 30}) -fmt.Println(lazy.HasAny(1, 5)) // true (index 1 exists) -``` - ---- - -## Retrieval - -### First - -```go -func (lc *LazyCollection[T]) First(predicates ...func(T, int) bool) (T, bool) -``` - -Returns the first element matching the optional predicate. If no predicate is given, returns the first element. The second return value indicates whether a match was found. - -```go -lazy := collection.LazyFrom([]int{1, 2, 3, 4, 5}) - -val, _ := lazy.First() // 1 - -val, _ = lazy.First(func(v int, _ int) bool { - return v > 3 -}) -// val=4 -``` - -**When to use:** Finding the first match without materializing the entire sequence. - ---- - -### FirstOrFail - -```go -func (lc *LazyCollection[T]) FirstOrFail(predicates ...func(T, int) bool) (T, error) -``` - -Like `First`, but returns an `ItemNotFoundError` instead of a boolean when no match exists. - -```go -lazy := collection.LazyFrom([]int{1, 2, 3}) - -val, err := lazy.FirstOrFail(func(v int, _ int) bool { - return v > 100 -}) -if err != nil { - fmt.Println(err) // "item not found" -} -``` - -**When to use:** When a missing element should be treated as an error rather than a normal condition. - ---- - -### Last - -```go -func (lc *LazyCollection[T]) Last(predicates ...func(T, int) bool) (T, bool) -``` - -Returns the last element matching the optional predicate. Requires a full pass through the sequence. - -```go -lazy := collection.LazyFrom([]int{1, 2, 3, 4, 5}) - -val, _ := lazy.Last() // 5 - -val, _ = lazy.Last(func(v int, _ int) bool { - return v%2 == 0 -}) -// val=4 -``` - ---- - -### Sole - -```go -func (lc *LazyCollection[T]) Sole(predicates ...func(T, int) bool) (T, error) -``` - -Returns the only element matching the optional predicate. Returns an error if zero or more than one element matches (`ItemNotFoundError` or `MultipleItemsFoundError`). - -```go -lazy := collection.LazyFrom([]int{1, 2, 3}) - -val, err := lazy.Sole(func(v int, _ int) bool { - return v == 2 -}) -// val=2, err=nil - -_, err = lazy.Sole(func(v int, _ int) bool { - return v > 1 -}) -// err: "multiple items found: 2 items" -``` - -**When to use:** Enforcing that exactly one item matches a condition. Useful for lookups that must be unique. - ---- - -### Get - -```go -func (lc *LazyCollection[T]) Get(index int) (T, bool) -``` - -Returns the item at the given zero-based index. Only iterates up to that index. - -```go -lazy := collection.LazyRange(10, 20) - -val, ok := lazy.Get(3) // val=13, ok=true -val, ok = lazy.Get(99) // ok=false -``` - ---- - -## Search - -### Contains - -```go -func (lc *LazyCollection[T]) Contains(predicate func(T, int) bool) bool -``` - -Reports whether any item satisfies the predicate. Stops iteration at the first match. - -```go -lazy := collection.LazyFrom([]string{"go", "rust", "zig"}) - -hasGo := lazy.Contains(func(s string, _ int) bool { - return s == "go" -}) -fmt.Println(hasGo) // true -``` - ---- - -### Some - -```go -func (lc *LazyCollection[T]) Some(predicate func(T, int) bool) bool -``` - -Alias for `Contains`. - ---- - -### DoesntContain - -```go -func (lc *LazyCollection[T]) DoesntContain(predicate func(T, int) bool) bool -``` - -Reports whether no item satisfies the predicate. The logical inverse of `Contains`. - -```go -lazy := collection.LazyFrom([]int{2, 4, 6}) - -noOdds := lazy.DoesntContain(func(v int, _ int) bool { - return v%2 != 0 -}) -fmt.Println(noOdds) // true -``` - ---- - -### HasSole - -```go -func (lc *LazyCollection[T]) HasSole(predicates ...func(T, int) bool) bool -``` - -Reports whether exactly one item matches the optional predicate. If no predicate is given, checks whether the collection has exactly one item total. - -```go -lazy := collection.LazyFrom([]int{1, 2, 3, 4, 5}) - -fmt.Println(lazy.HasSole(func(v int, _ int) bool { - return v == 3 -})) // true - -fmt.Println(lazy.HasSole(func(v int, _ int) bool { - return v > 3 -})) // false (two items match: 4 and 5) -``` - ---- - -### Search - -```go -func (lc *LazyCollection[T]) Search(predicate func(T, int) bool) (int, bool) -``` - -Returns the index of the first item satisfying the predicate. Stops iteration at the match. - -```go -lazy := collection.LazyFrom([]string{"a", "b", "c", "d"}) - -idx, found := lazy.Search(func(s string, _ int) bool { - return s == "c" -}) -fmt.Println(idx, found) // 2 true -``` - ---- - -### Before - -```go -func (lc *LazyCollection[T]) Before(predicate func(T, int) bool) (T, bool) -``` - -Returns the item immediately before the first item matching the predicate. Returns `false` if the match is the first item or no match is found. - -```go -lazy := collection.LazyFrom([]int{10, 20, 30, 40}) - -val, ok := lazy.Before(func(v int, _ int) bool { - return v == 30 -}) -fmt.Println(val, ok) // 20 true -``` - -**When to use:** Finding the predecessor of an element in a sequence -- for example, the previous step in a pipeline. - ---- - -### After - -```go -func (lc *LazyCollection[T]) After(predicate func(T, int) bool) (T, bool) -``` - -Returns the item immediately after the first item matching the predicate. Returns `false` if the match is the last item or no match is found. - -```go -lazy := collection.LazyFrom([]int{10, 20, 30, 40}) - -val, ok := lazy.After(func(v int, _ int) bool { - return v == 20 -}) -fmt.Println(val, ok) // 30 true -``` - ---- - -## Iteration - -### Each - -```go -func (lc *LazyCollection[T]) Each(callback func(T, int) bool) *LazyCollection[T] -``` - -Iterates over items, calling the callback for each one with the item and its zero-based index. Return `false` from the callback to stop early. - -```go -collection.LazyFrom([]string{"a", "b", "c"}).Each(func(s string, i int) bool { - fmt.Printf("[%d] %s\n", i, s) - return true -}) -// Output: -// [0] a -// [1] b -// [2] c -``` - -> **Note:** `Each` evaluates the sequence. It returns the original lazy collection, not a new one. - ---- - -### Tap - -```go -func (lc *LazyCollection[T]) Tap(callback func(*LazyCollection[T])) *LazyCollection[T] -``` - -Passes the lazy collection to the callback for side effects and returns it unchanged. - -```go -result := collection.LazyFrom([]int{1, 2, 3}). - Tap(func(lc *collection.LazyCollection[int]) { - fmt.Println("before filter:", lc.Count()) - }). - Filter(func(v int, _ int) bool { return v > 1 }) -``` - ---- - -### TapEach - -```go -func (lc *LazyCollection[T]) TapEach(callback func(T, int)) *LazyCollection[T] -``` - -Returns a new lazy collection that calls the callback on each item as it passes through the pipeline. Unlike `Each`, `TapEach` is lazy -- the callback only fires when the resulting collection is consumed. - -```go -result := collection.LazyFrom([]int{1, 2, 3}). - TapEach(func(v int, i int) { - fmt.Printf("processing %d at index %d\n", v, i) - }). - Filter(func(v int, _ int) bool { return v > 1 }). - All() -// "processing" is printed for each item as it flows through -``` - -**When to use:** Adding logging or metrics to a lazy pipeline without breaking the chain or forcing evaluation. - ---- - -## Filtering - -### Filter - -```go -func (lc *LazyCollection[T]) Filter(callback func(T, int) bool) *LazyCollection[T] -``` - -Returns a new lazy collection containing only items for which the callback returns `true`. The filter is applied lazily -- items are tested one at a time as they are consumed. - -```go -evens := collection.LazyRange(1, 100).Filter(func(v int, _ int) bool { - return v%2 == 0 -}) -first5 := evens.Take(5).All() // [2, 4, 6, 8, 10] -``` - ---- - -### Reject - -```go -func (lc *LazyCollection[T]) Reject(callback func(T, int) bool) *LazyCollection[T] -``` - -Returns a new lazy collection excluding items for which the callback returns `true`. The logical inverse of `Filter`. - -```go -noNegatives := collection.LazyFrom([]int{-2, -1, 0, 1, 2}). - Reject(func(v int, _ int) bool { return v < 0 }) -fmt.Println(noNegatives.All()) // [0, 1, 2] -``` - ---- - -## Transformation - -### LazyMap (top-level function) - -```go -func LazyMap[T any, R any]( - lc *LazyCollection[T], - callback func(T, int) R, -) *LazyCollection[R] -``` - -Transforms each item using the callback, returning a new lazy collection of the transformed type. Because Go methods cannot introduce new type parameters, this is a package-level function. - -```go -names := collection.LazyFrom([]string{"alice", "bob"}) - -lengths := collection.LazyMap(names, func(s string, _ int) int { - return len(s) -}) -fmt.Println(lengths.All()) // [5, 3] -``` - -**When to use:** Mapping between types in a lazy pipeline -- for example, converting raw records to domain objects, or extracting a single field. - ---- - -### LazyFlatMap (top-level function) - -```go -func LazyFlatMap[T any, R any]( - lc *LazyCollection[T], - callback func(T, int) []R, -) *LazyCollection[R] -``` - -Transforms each item into a slice and flattens the results into a single lazy sequence. - -```go -sentences := collection.LazyFrom([]string{"hello world", "foo bar"}) - -words := collection.LazyFlatMap(sentences, func(s string, _ int) []string { - return strings.Split(s, " ") -}) -fmt.Println(words.All()) // ["hello", "world", "foo", "bar"] -``` - -**When to use:** One-to-many transformations where each input produces a variable number of outputs. - ---- - -### LazyReduce (top-level function) - -```go -func LazyReduce[T any, R any]( - lc *LazyCollection[T], - callback func(R, T, int) R, - initial R, -) R -``` - -Reduces the lazy collection to a single value by applying the callback to an accumulator and each item. Eagerly evaluates the entire sequence. - -```go -sum := collection.LazyReduce( - collection.LazyRange(1, 100), - func(acc int, val int, _ int) int { return acc + val }, - 0, -) -fmt.Println(sum) // 5050 -``` - ---- - -### LazyUnique (top-level function) - -```go -func LazyUnique[T any, K comparable]( - lc *LazyCollection[T], - keyFunc func(T) K, -) *LazyCollection[T] -``` - -Returns a lazy collection containing only unique items as determined by the key function. The first occurrence of each key is kept. - -```go -type User struct { - ID int - Name string -} - -users := collection.LazyFrom([]User{ - {1, "Alice"}, {2, "Bob"}, {1, "Alice (dup)"}, -}) - -unique := collection.LazyUnique(users, func(u User) int { - return u.ID -}) -fmt.Println(unique.Count()) // 2 -``` - -**When to use:** Deduplicating a stream by a key without sorting. - ---- - -### LazyPluck (top-level function) - -```go -func LazyPluck[T any, V any]( - lc *LazyCollection[T], - valueFunc func(T) V, -) *LazyCollection[V] -``` - -Extracts a value from each item, returning a new lazy collection of the extracted values. This is a convenience wrapper around `LazyMap`. - -```go -type Product struct { - Name string - Price float64 -} - -products := collection.LazyFrom([]Product{ - {"Widget", 9.99}, - {"Gadget", 24.99}, -}) - -prices := collection.LazyPluck(products, func(p Product) float64 { - return p.Price -}) -fmt.Println(prices.All()) // [9.99, 24.99] -``` - ---- - -## Partitioning - -### Chunk - -```go -func (lc *LazyCollection[T]) Chunk(size int) [][]T -``` - -Eagerly evaluates the lazy collection and splits items into groups of the given size. The last group may contain fewer items. - -```go -chunks := collection.LazyRange(1, 7).Chunk(3) -// [[1, 2, 3], [4, 5, 6], [7]] -``` - -**When to use:** Batch processing -- for example, sending items to an API in groups of N. - ---- - -### ChunkWhile - -```go -func (lc *LazyCollection[T]) ChunkWhile( - callback func(T, int, []T) bool, -) [][]T -``` - -Splits the lazy collection into groups where consecutive items satisfy the callback. A new chunk is started when the callback returns `false`. The callback receives the current item, its index, and the current chunk being built. - -```go -// Group consecutive even/odd numbers together -chunks := collection.LazyFrom([]int{2, 4, 6, 1, 3, 8, 10}). - ChunkWhile(func(val int, _ int, current []int) bool { - return val%2 == current[0]%2 - }) -// [[2, 4, 6], [1, 3], [8, 10]] -``` - ---- - -### Nth - -```go -func (lc *LazyCollection[T]) Nth(step int, offsets ...int) *LazyCollection[T] -``` - -Returns every `step`-th element, starting from an optional offset (default 0). The result is lazy. - -```go -every3rd := collection.LazyRange(1, 12).Nth(3) -fmt.Println(every3rd.All()) // [1, 4, 7, 10] - -// With offset -every3rdFrom1 := collection.LazyRange(1, 12).Nth(3, 1) -fmt.Println(every3rdFrom1.All()) // [2, 5, 8, 11] -``` - -**When to use:** Sampling data at regular intervals or implementing striped processing. - ---- - -## Slicing - -### Take - -```go -func (lc *LazyCollection[T]) Take(limit int) *LazyCollection[T] -``` - -Returns a new lazy collection with at most `limit` items from the beginning. A negative limit takes from the end (which requires eager evaluation of the entire sequence). - -```go -first3 := collection.LazyRange(1, 1000).Take(3) -fmt.Println(first3.All()) // [1, 2, 3] - -last3 := collection.LazyRange(1, 10).Take(-3) -fmt.Println(last3.All()) // [8, 9, 10] -``` - -**When to use:** Limiting results, implementing pagination, or previewing the first N items of a large dataset. - ---- - -### TakeUntil - -```go -func (lc *LazyCollection[T]) TakeUntil(callback func(T, int) bool) *LazyCollection[T] -``` - -Returns items from the beginning until the callback returns `true`. The item that triggers `true` is **not** included. - -```go -items := collection.LazyFrom([]int{1, 2, 3, 4, 5}). - TakeUntil(func(v int, _ int) bool { return v == 4 }) -fmt.Println(items.All()) // [1, 2, 3] -``` - ---- - -### TakeWhile - -```go -func (lc *LazyCollection[T]) TakeWhile(callback func(T, int) bool) *LazyCollection[T] -``` - -Returns items from the beginning while the callback returns `true`. Stops at the first `false`. - -```go -items := collection.LazyFrom([]int{2, 4, 6, 7, 8}). - TakeWhile(func(v int, _ int) bool { return v%2 == 0 }) -fmt.Println(items.All()) // [2, 4, 6] -``` - ---- - -### TakeUntilTimeout - -```go -func (lc *LazyCollection[T]) TakeUntilTimeout(timeout time.Duration) *LazyCollection[T] -``` - -Returns items until the given duration has elapsed. Useful for processing streams with a time budget. - -```go -import "time" - -// Process items for at most 2 seconds -results := someLazyStream.TakeUntilTimeout(2 * time.Second).All() -``` - -**When to use:** Rate-limited consumption, polling with a deadline, or any scenario where processing time matters more than item count. - ---- - -### Skip - -```go -func (lc *LazyCollection[T]) Skip(count int) *LazyCollection[T] -``` - -Returns a new lazy collection that skips the first `count` items. - -```go -items := collection.LazyRange(1, 10).Skip(5) -fmt.Println(items.All()) // [6, 7, 8, 9, 10] -``` - ---- - -### SkipUntil - -```go -func (lc *LazyCollection[T]) SkipUntil(callback func(T, int) bool) *LazyCollection[T] -``` - -Skips items until the callback returns `true`, then yields that item and all subsequent items. - -```go -items := collection.LazyFrom([]int{1, 2, 3, 4, 5}). - SkipUntil(func(v int, _ int) bool { return v >= 3 }) -fmt.Println(items.All()) // [3, 4, 5] -``` - ---- - -### SkipWhile - -```go -func (lc *LazyCollection[T]) SkipWhile(callback func(T, int) bool) *LazyCollection[T] -``` - -Skips items while the callback returns `true`, then yields the rest. - -```go -items := collection.LazyFrom([]int{1, 2, 3, 4, 5}). - SkipWhile(func(v int, _ int) bool { return v < 3 }) -fmt.Println(items.All()) // [3, 4, 5] -``` - ---- - -### Slice - -```go -func (lc *LazyCollection[T]) Slice(offset int, lengths ...int) *LazyCollection[T] -``` - -Returns a subset of the lazy collection starting at `offset` with an optional length. Equivalent to chaining `Skip(offset).Take(length)`. - -```go -items := collection.LazyRange(1, 20).Slice(5, 3) -fmt.Println(items.All()) // [6, 7, 8] -``` - ---- - -## Combining - -### Concat - -```go -func (lc *LazyCollection[T]) Concat(items []T) *LazyCollection[T] -``` - -Returns a new lazy collection with the given items appended after the current sequence. - -```go -combined := collection.LazyFrom([]int{1, 2}).Concat([]int{3, 4, 5}) -fmt.Println(combined.All()) // [1, 2, 3, 4, 5] -``` - ---- - -### Pad - -```go -func (lc *LazyCollection[T]) Pad(size int, value T) *LazyCollection[T] -``` - -Returns a new lazy collection padded to the given size with the specified value. -- **Positive size:** pads at the end. -- **Negative size:** pads at the beginning (requires eager evaluation). - -```go -padded := collection.LazyFrom([]int{1, 2}).Pad(5, 0) -fmt.Println(padded.All()) // [1, 2, 0, 0, 0] - -leftPadded := collection.LazyFrom([]int{1, 2}).Pad(-5, 0) -fmt.Println(leftPadded.All()) // [0, 0, 0, 1, 2] -``` - -**When to use:** Ensuring a minimum length -- for example, filling a table row to a fixed column count. - ---- - -## Grouping - -### LazyGroupBy (top-level function) - -```go -func LazyGroupBy[T any, K comparable]( - lc *LazyCollection[T], - keyFunc func(T) K, -) map[K]*LazyCollection[T] -``` - -Groups items by the key returned by the given function. This requires eager evaluation to build the groups. Each group is returned as a `LazyCollection`. - -```go -type Order struct { - Status string - Total float64 -} - -orders := collection.LazyFrom([]Order{ - {"pending", 100}, {"shipped", 200}, {"pending", 50}, -}) - -grouped := collection.LazyGroupBy(orders, func(o Order) string { - return o.Status -}) - -pending := grouped["pending"].All() -// [{pending 100}, {pending 50}] -``` - ---- - -### LazyKeyBy (top-level function) - -```go -func LazyKeyBy[T any, K comparable]( - lc *LazyCollection[T], - keyFunc func(T) K, -) map[K]T -``` - -Indexes items by the key returned by the given function. If duplicate keys exist, the later value overwrites the earlier one. - -```go -type User struct { - ID int - Name string -} - -users := collection.LazyFrom([]User{{1, "Alice"}, {2, "Bob"}}) - -byID := collection.LazyKeyBy(users, func(u User) int { - return u.ID -}) -fmt.Println(byID[1].Name) // "Alice" -``` - ---- - -### LazyCountBy (top-level function) - -```go -func LazyCountBy[T any, K comparable]( - lc *LazyCollection[T], - keyFunc func(T) K, -) map[K]int -``` - -Counts occurrences of each key returned by the given function. - -```go -words := collection.LazyFrom([]string{"go", "rust", "go", "zig", "go"}) - -counts := collection.LazyCountBy(words, func(s string) string { - return s -}) -fmt.Println(counts) // map[go:3 rust:1 zig:1] -``` - ---- - -## Caching - -### Remember - -```go -func (lc *LazyCollection[T]) Remember() *LazyCollection[T] -``` - -Returns a lazy collection that caches items on first iteration. Subsequent iterations reuse the cached values instead of re-evaluating the source. - -```go -expensive := collection.NewLazy(func(yield func(int) bool) { - for i := 0; i < 3; i++ { - fmt.Println("computing", i) - if !yield(i) { - return - } - } -}).Remember() - -expensive.All() // prints "computing 0", "computing 1", "computing 2" -expensive.All() // no output -- results are cached -``` - -**When to use:** When the source is expensive to evaluate (I/O, network calls, heavy computation) and you need to iterate more than once. - ---- - -### Throttle - -```go -func (lc *LazyCollection[T]) Throttle(delay time.Duration) *LazyCollection[T] -``` - -Returns a new lazy collection that inserts a delay between each yielded item. The delay applies lazily as items are consumed. - -```go -import "time" - -// Rate-limit API calls to 1 per second -results := collection.LazyFrom(apiRequests). - Throttle(time.Second). - All() -``` - -**When to use:** Rate limiting, gentle polling, or preventing thundering-herd issues when processing items that trigger external calls. - ---- - -## String - -### Implode - -```go -func (lc *LazyCollection[T]) Implode(glue string) string -``` - -Joins all items into a single string separated by the given glue. Items are converted to strings using `fmt.Sprint`. - -```go -result := collection.LazyFrom([]int{1, 2, 3}).Implode(", ") -fmt.Println(result) // "1, 2, 3" -``` - ---- - -### Join - -```go -func (lc *LazyCollection[T]) Join(glue string, finalGlues ...string) string -``` - -Like `Implode`, but with an optional final glue placed between the last two items. - -```go -result := collection.LazyFrom([]string{"Go", "Rust", "Zig"}). - Join(", ", ", and ") -fmt.Println(result) // "Go, Rust, and Zig" -``` - ---- - -## Conditional - -### When - -```go -func (lc *LazyCollection[T]) When( - condition bool, - callback func(*LazyCollection[T]) *LazyCollection[T], - defaults ...func(*LazyCollection[T]) *LazyCollection[T], -) *LazyCollection[T] -``` - -Applies the callback if the condition is `true`. An optional default callback is applied when the condition is `false`. - -```go -includeArchived := false - -orders := collection.LazyFrom(allOrders). - When(includeArchived, func(lc *collection.LazyCollection[Order]) *collection.LazyCollection[Order] { - return lc // include everything - }, func(lc *collection.LazyCollection[Order]) *collection.LazyCollection[Order] { - return lc.Filter(func(o Order, _ int) bool { - return o.Status != "archived" - }) - }) -``` - -**When to use:** Conditionally modifying a pipeline without breaking the method chain. - ---- - -### WhenEmpty - -```go -func (lc *LazyCollection[T]) WhenEmpty( - callback func(*LazyCollection[T]) *LazyCollection[T], - defaults ...func(*LazyCollection[T]) *LazyCollection[T], -) *LazyCollection[T] -``` - -Applies the callback if the lazy collection is empty. - -```go -results := fetchResults(). - WhenEmpty(func(lc *collection.LazyCollection[Result]) *collection.LazyCollection[Result] { - return collection.LazyFrom(defaultResults) - }) -``` - ---- - -### WhenNotEmpty - -```go -func (lc *LazyCollection[T]) WhenNotEmpty( - callback func(*LazyCollection[T]) *LazyCollection[T], - defaults ...func(*LazyCollection[T]) *LazyCollection[T], -) *LazyCollection[T] -``` - -Applies the callback if the lazy collection is not empty. - ---- - -### Unless - -```go -func (lc *LazyCollection[T]) Unless( - condition bool, - callback func(*LazyCollection[T]) *LazyCollection[T], - defaults ...func(*LazyCollection[T]) *LazyCollection[T], -) *LazyCollection[T] -``` - -The inverse of `When`: applies the callback unless the condition is `true`. - ---- - -## Other - -### Every - -```go -func (lc *LazyCollection[T]) Every(callback func(T, int) bool) bool -``` - -Reports whether all items satisfy the callback. Stops iteration at the first failure. - -```go -allPositive := collection.LazyFrom([]int{1, 2, 3, 4}). - Every(func(v int, _ int) bool { return v > 0 }) -fmt.Println(allPositive) // true -``` - ---- - -### Dump - -```go -func (lc *LazyCollection[T]) Dump() *LazyCollection[T] -``` - -Prints the items to stdout for debugging. Eagerly evaluates the sequence and returns a new lazy collection backed by the evaluated items. - -```go -collection.LazyFrom([]int{1, 2, 3}).Dump() -// Output: [1 2 3] -``` - -> **Note:** Because `Dump` must evaluate the sequence to print it, the returned collection is backed by the materialized slice, not the original source. diff --git a/docs/map_collection.md b/docs/map_collection.md deleted file mode 100644 index dc91158..0000000 --- a/docs/map_collection.md +++ /dev/null @@ -1,1104 +0,0 @@ -# MapCollection[K, V] - -`MapCollection` is an ordered map that preserves insertion order. Unlike Go's built-in `map`, iterating over a `MapCollection` always visits entries in the order they were added. It provides a fluent, chainable API for querying, filtering, transforming, and serializing key-value data. - -```go -import "github.com/gocanto/collection/collection" -``` - ---- - -## Table of Contents - -- [Constructors](#constructors) -- [Query](#query) -- [Retrieval](#retrieval) -- [Search](#search) -- [Mutation](#mutation) -- [Filtering](#filtering) -- [Transformation](#transformation) -- [Sorting](#sorting) -- [Iteration](#iteration) -- [String](#string) -- [Conditional](#conditional) -- [Partitioning](#partitioning) -- [Serialization](#serialization) - ---- - -## Constructors - -### NewMap - -```go -func NewMap[K comparable, V any](items map[K]V) *MapCollection[K, V] -``` - -Creates a `MapCollection` from an existing Go map. If `nil` is passed, an empty collection is created. - -```go -m := collection.NewMap(map[string]int{ - "apples": 5, - "oranges": 3, -}) -fmt.Println(m.Count()) // 2 -``` - -**When to use:** You already have a `map` and want the ordered, fluent API that `MapCollection` provides. - ---- - -### NewMapFromPairs - -```go -func NewMapFromPairs[K comparable, V any](pairs ...Pair[K, V]) *MapCollection[K, V] -``` - -Creates a `MapCollection` from explicit key-value pairs. Insertion order is determined by the order of the arguments. If a key appears more than once, the last value wins but the key retains its first position. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "a", Value: 1}, - collection.Pair[string, int]{Key: "b", Value: 2}, - collection.Pair[string, int]{Key: "c", Value: 3}, -) -fmt.Println(m.Keys().All()) // [a b c] -``` - -**When to use:** You need guaranteed insertion order from the start, or you are constructing a map programmatically from pairs. - ---- - -## Query - -### All - -```go -func (m *MapCollection[K, V]) All() map[K]V -``` - -Returns a shallow copy of the underlying map. Changes to the returned map do not affect the collection. - -```go -m := collection.NewMap(map[string]int{"x": 10, "y": 20}) -raw := m.All() // map[string]int{"x": 10, "y": 20} -``` - -**When to use:** You need to pass the data to code that expects a plain `map`. - ---- - -### Keys - -```go -func (m *MapCollection[K, V]) Keys() *Collection[K] -``` - -Returns all keys as a `Collection`, preserving insertion order. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "first", Value: 1}, - collection.Pair[string, int]{Key: "second", Value: 2}, -) -keys := m.Keys().All() // ["first", "second"] -``` - -**When to use:** You need to iterate over or manipulate just the keys -- for example, checking for duplicates or sorting them independently. - ---- - -### Values - -```go -func (m *MapCollection[K, V]) Values() *Collection[V] -``` - -Returns all values as a `Collection`, preserving insertion order. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "a", Value: 100}, - collection.Pair[string, int]{Key: "b", Value: 200}, -) -vals := m.Values().All() // [100, 200] -``` - -**When to use:** You want to aggregate, filter, or transform only the values without caring about keys. - ---- - -### Count - -```go -func (m *MapCollection[K, V]) Count() int -``` - -Returns the number of items in the collection. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) -fmt.Println(m.Count()) // 3 -``` - ---- - -### IsEmpty - -```go -func (m *MapCollection[K, V]) IsEmpty() bool -``` - -Reports whether the collection contains no items. - -```go -m := collection.NewMap(map[string]int{}) -fmt.Println(m.IsEmpty()) // true -``` - ---- - -### IsNotEmpty - -```go -func (m *MapCollection[K, V]) IsNotEmpty() bool -``` - -Reports whether the collection contains at least one item. - -```go -m := collection.NewMap(map[string]int{"a": 1}) -fmt.Println(m.IsNotEmpty()) // true -``` - ---- - -### Has - -```go -func (m *MapCollection[K, V]) Has(key K) bool -``` - -Reports whether the given key exists in the collection. - -```go -m := collection.NewMap(map[string]int{"name": 42}) -fmt.Println(m.Has("name")) // true -fmt.Println(m.Has("email")) // false -``` - -**When to use:** Quick key-existence check before performing an operation. - ---- - -### HasAny - -```go -func (m *MapCollection[K, V]) HasAny(keys ...K) bool -``` - -Reports whether any of the given keys exist in the collection. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2}) -fmt.Println(m.HasAny("b", "z")) // true -fmt.Println(m.HasAny("x", "z")) // false -``` - -**When to use:** Validating that at least one of several expected keys is present, such as checking for alternative field names in a config. - ---- - -### ContainsOneItem - -```go -func (m *MapCollection[K, V]) ContainsOneItem() bool -``` - -Reports whether the collection contains exactly one item. - -```go -m := collection.NewMap(map[string]int{"only": 1}) -fmt.Println(m.ContainsOneItem()) // true -``` - ---- - -### ContainsManyItems - -```go -func (m *MapCollection[K, V]) ContainsManyItems() bool -``` - -Reports whether the collection contains more than one item. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2}) -fmt.Println(m.ContainsManyItems()) // true -``` - ---- - -## Retrieval - -### Get - -```go -func (m *MapCollection[K, V]) Get(key K, defaults ...V) (V, bool) -``` - -Returns the value for the given key. The second return value indicates whether the key was found. An optional default value is returned (with `false`) when the key is missing. - -```go -m := collection.NewMap(map[string]int{"timeout": 30}) - -val, ok := m.Get("timeout") -// val=30, ok=true - -val, ok = m.Get("retries", 3) -// val=3, ok=false (key missing, default returned) -``` - -**When to use:** Safe value retrieval with an optional fallback, similar to Python's `dict.get(key, default)`. - ---- - -### GetOrPut - -```go -func (m *MapCollection[K, V]) GetOrPut(key K, value V) V -``` - -Returns the value for the given key if it exists. Otherwise stores and returns the provided default value. - -```go -m := collection.NewMap(map[string]int{"hits": 10}) - -val := m.GetOrPut("hits", 0) // 10 (existing value) -val = m.GetOrPut("misses", 0) // 0 (inserted and returned) -fmt.Println(m.Has("misses")) // true -``` - -**When to use:** Implementing a "get or initialize" pattern -- for example, building a counter map where missing keys default to zero. - ---- - -### First - -```go -func (m *MapCollection[K, V]) First(predicates ...func(V, K) bool) (V, bool) -``` - -Returns the first value in insertion order. If a predicate is provided, returns the first value satisfying it. The second return value indicates whether a match was found. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "a", Value: 10}, - collection.Pair[string, int]{Key: "b", Value: 20}, - collection.Pair[string, int]{Key: "c", Value: 30}, -) - -val, _ := m.First() // 10 - -val, _ = m.First(func(v int, k string) bool { - return v > 15 -}) -// val=20 -``` - ---- - -### Last - -```go -func (m *MapCollection[K, V]) Last(predicates ...func(V, K) bool) (V, bool) -``` - -Returns the last value in insertion order, or the last value satisfying the optional predicate. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "a", Value: 10}, - collection.Pair[string, int]{Key: "b", Value: 20}, - collection.Pair[string, int]{Key: "c", Value: 30}, -) - -val, _ := m.Last() // 30 - -val, _ = m.Last(func(v int, k string) bool { - return v < 25 -}) -// val=20 -``` - ---- - -## Search - -### Contains - -```go -func (m *MapCollection[K, V]) Contains(predicate func(V, K) bool) bool -``` - -Reports whether any item in the collection satisfies the predicate. - -```go -m := collection.NewMap(map[string]int{"age": 25, "score": 90}) - -hasHighScore := m.Contains(func(v int, k string) bool { - return k == "score" && v >= 80 -}) -fmt.Println(hasHighScore) // true -``` - -**When to use:** Checking for the existence of a value that meets a complex condition involving both the key and the value. - ---- - -### Some - -```go -func (m *MapCollection[K, V]) Some(predicate func(V, K) bool) bool -``` - -Alias for `Contains`. Use whichever reads more naturally in your code. - ---- - -### DoesntContain - -```go -func (m *MapCollection[K, V]) DoesntContain(predicate func(V, K) bool) bool -``` - -Reports whether no item in the collection satisfies the predicate. The logical inverse of `Contains`. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2}) - -noNegatives := m.DoesntContain(func(v int, _ string) bool { - return v < 0 -}) -fmt.Println(noNegatives) // true -``` - ---- - -### HasSole - -```go -func (m *MapCollection[K, V]) HasSole(predicate func(V, K) bool) bool -``` - -Reports whether exactly one item in the collection satisfies the predicate. - -```go -m := collection.NewMap(map[string]string{ - "role": "admin", - "name": "Alice", - "dept": "Engineering", -}) - -onlyOneAdmin := m.HasSole(func(v string, k string) bool { - return v == "admin" -}) -fmt.Println(onlyOneAdmin) // true -``` - -**When to use:** Asserting uniqueness constraints -- for example, ensuring exactly one entry matches a given criterion. - ---- - -### Search - -```go -func (m *MapCollection[K, V]) Search(predicate func(V, K) bool) (K, bool) -``` - -Returns the key of the first item that satisfies the predicate. The second return value indicates whether a match was found. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "x", Value: 100}, - collection.Pair[string, int]{Key: "y", Value: 200}, - collection.Pair[string, int]{Key: "z", Value: 300}, -) - -key, found := m.Search(func(v int, _ string) bool { - return v == 200 -}) -fmt.Println(key, found) // y true -``` - ---- - -## Mutation - -### Put - -```go -func (m *MapCollection[K, V]) Put(key K, value V) *MapCollection[K, V] -``` - -Sets a key-value pair. If the key already exists, the value is updated in place. If the key is new, it is appended to the end of the insertion-order list. Returns the collection for chaining. - -```go -m := collection.NewMap(map[string]int{}) -m.Put("x", 1).Put("y", 2).Put("z", 3) -fmt.Println(m.Count()) // 3 -``` - ---- - -### Pull - -```go -func (m *MapCollection[K, V]) Pull(key K) (V, bool) -``` - -Removes an item by key and returns its value. The second return value indicates whether the key was found. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2}) - -val, ok := m.Pull("a") // val=1, ok=true -fmt.Println(m.Count()) // 1 -``` - -**When to use:** Extracting a value while simultaneously removing it, like popping from a dictionary. - ---- - -### Forget - -```go -func (m *MapCollection[K, V]) Forget(keys ...K) *MapCollection[K, V] -``` - -Removes one or more items by key. Returns the collection for chaining. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) -m.Forget("a", "c") -fmt.Println(m.Count()) // 1 -``` - -**When to use:** Bulk removal of keys you no longer need, such as stripping internal fields before returning data to a caller. - ---- - -## Filtering - -### Only - -```go -func (m *MapCollection[K, V]) Only(keys ...K) *MapCollection[K, V] -``` - -Returns a new collection containing only the specified keys. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}) -subset := m.Only("a", "c") -fmt.Println(subset.All()) // map[a:1 c:3] -``` - -**When to use:** Picking a subset of fields -- for example, selecting only the columns you need from a record. - ---- - -### Except - -```go -func (m *MapCollection[K, V]) Except(keys ...K) *MapCollection[K, V] -``` - -Returns a new collection containing all items except those with the specified keys. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) -rest := m.Except("b") -fmt.Println(rest.All()) // map[a:1 c:3] -``` - -**When to use:** The inverse of `Only` -- hiding sensitive or irrelevant fields. - ---- - -### Filter - -```go -func (m *MapCollection[K, V]) Filter(callback func(V, K) bool) *MapCollection[K, V] -``` - -Returns a new collection containing only the items for which the callback returns `true`. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 5, "c": 10, "d": 15}) - -big := m.Filter(func(v int, _ string) bool { - return v >= 10 -}) -fmt.Println(big.All()) // map[c:10 d:15] -``` - -**When to use:** General-purpose value/key filtering with a custom predicate. - ---- - -### Reject - -```go -func (m *MapCollection[K, V]) Reject(callback func(V, K) bool) *MapCollection[K, V] -``` - -Returns a new collection excluding items for which the callback returns `true`. The logical inverse of `Filter`. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 5, "c": 10}) - -small := m.Reject(func(v int, _ string) bool { - return v >= 10 -}) -fmt.Println(small.All()) // map[a:1 b:5] -``` - ---- - -## Transformation - -### MapValues (top-level function) - -```go -func MapValues[K comparable, V any, R any]( - m *MapCollection[K, V], - callback func(V, K) R, -) *MapCollection[K, R] -``` - -Transforms each value using the callback, returning a new `MapCollection` with the same keys and transformed values. Because Go does not allow methods to introduce new type parameters, this is a package-level function. - -```go -prices := collection.NewMap(map[string]float64{ - "apple": 1.20, - "banana": 0.50, -}) - -doubled := collection.MapValues(prices, func(v float64, _ string) float64 { - return v * 2 -}) -fmt.Println(doubled.All()) // map[apple:2.4 banana:1] -``` - -**When to use:** Applying a uniform transformation to all values -- currency conversion, unit changes, formatting. - ---- - -### MapFlip (top-level function) - -```go -func MapFlip[K comparable, V comparable]( - m *MapCollection[K, V], -) *MapCollection[V, K] -``` - -Swaps keys and values. The original values become keys and original keys become values. Both `K` and `V` must be `comparable`. If duplicate values exist, the last key wins. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) -flipped := collection.MapFlip(m) -// flipped: map[1:"a", 2:"b", 3:"c"] -``` - -**When to use:** Building a reverse lookup table. - ---- - -### Merge - -```go -func (m *MapCollection[K, V]) Merge(items map[K]V) *MapCollection[K, V] -``` - -Returns a new collection with the given map merged in. Existing keys are overwritten by the incoming values. New keys are appended. - -```go -base := collection.NewMap(map[string]int{"a": 1, "b": 2}) -merged := base.Merge(map[string]int{"b": 20, "c": 30}) -fmt.Println(merged.All()) // map[a:1 b:20 c:30] -``` - ---- - -### Union - -```go -func (m *MapCollection[K, V]) Union(items map[K]V) *MapCollection[K, V] -``` - -Returns a new collection that is the union of the collection and the given map. Keys already present in the original collection are **not** overwritten. - -```go -base := collection.NewMap(map[string]int{"a": 1, "b": 2}) -united := base.Union(map[string]int{"b": 20, "c": 30}) -fmt.Println(united.All()) // map[a:1 b:2 c:30] ("b" kept original value) -``` - -**When to use:** Providing defaults -- the incoming map supplies fallback values for missing keys without overriding existing ones. - ---- - -### Replace - -```go -func (m *MapCollection[K, V]) Replace(items map[K]V) *MapCollection[K, V] -``` - -Alias for `Merge`. Existing keys are overwritten by the incoming values. - ---- - -### DiffKeys - -```go -func (m *MapCollection[K, V]) DiffKeys(items map[K]V) *MapCollection[K, V] -``` - -Returns a new collection containing items whose keys are **not** present in the given map. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) -diff := m.DiffKeys(map[string]int{"a": 0, "c": 0}) -fmt.Println(diff.All()) // map[b:2] -``` - -**When to use:** Finding entries that are unique to one dataset compared to another. - ---- - -### IntersectByKeys - -```go -func (m *MapCollection[K, V]) IntersectByKeys(items map[K]V) *MapCollection[K, V] -``` - -Returns a new collection containing items whose keys **are** present in the given map. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2, "c": 3}) -inter := m.IntersectByKeys(map[string]int{"a": 0, "c": 0}) -fmt.Println(inter.All()) // map[a:1 c:3] -``` - -**When to use:** Restricting a collection to only the keys that exist in another dataset. - ---- - -## Sorting - -### MapSortKeys (top-level function) - -```go -func MapSortKeys[V any](m *MapCollection[string, V]) *MapCollection[string, V] -``` - -Returns a new collection with string keys sorted in ascending order. - -```go -m := collection.NewMap(map[string]int{"banana": 2, "apple": 1, "cherry": 3}) -sorted := collection.MapSortKeys(m) -fmt.Println(sorted.Keys().All()) // [apple banana cherry] -``` - -> **Note:** This function works only with `string` keys. For other key types, use `SortKeysUsing`. - ---- - -### MapSortKeysDesc (top-level function) - -```go -func MapSortKeysDesc[V any](m *MapCollection[string, V]) *MapCollection[string, V] -``` - -Returns a new collection with string keys sorted in descending order. - -```go -m := collection.NewMap(map[string]int{"banana": 2, "apple": 1, "cherry": 3}) -sorted := collection.MapSortKeysDesc(m) -fmt.Println(sorted.Keys().All()) // [cherry banana apple] -``` - ---- - -### SortKeysUsing - -```go -func (m *MapCollection[K, V]) SortKeysUsing(less func(K, K) bool) *MapCollection[K, V] -``` - -Returns a new collection with keys sorted using the provided comparison function. Works with any comparable key type. - -```go -m := collection.NewMap(map[int]string{3: "c", 1: "a", 2: "b"}) -sorted := m.SortKeysUsing(func(a, b int) bool { - return a < b -}) -fmt.Println(sorted.Keys().All()) // [1 2 3] -``` - -**When to use:** Sorting by non-string keys or custom orderings (e.g., case-insensitive sort). - ---- - -## Iteration - -### Each - -```go -func (m *MapCollection[K, V]) Each(callback func(V, K) bool) *MapCollection[K, V] -``` - -Iterates over items in insertion order, calling the callback for each key-value pair. Return `false` from the callback to stop iteration early. Returns the collection for chaining. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "a", Value: 1}, - collection.Pair[string, int]{Key: "b", Value: 2}, - collection.Pair[string, int]{Key: "c", Value: 3}, -) - -m.Each(func(v int, k string) bool { - fmt.Printf("%s=%d\n", k, v) - return true // continue -}) -// Output: -// a=1 -// b=2 -// c=3 -``` - ---- - -### Tap - -```go -func (m *MapCollection[K, V]) Tap(callback func(*MapCollection[K, V])) *MapCollection[K, V] -``` - -Passes the collection to the callback for side effects (logging, debugging) and returns the collection unchanged, preserving a method chain. - -```go -result := collection.NewMap(map[string]int{"a": 1, "b": 2}). - Tap(func(m *collection.MapCollection[string, int]) { - fmt.Println("count:", m.Count()) - }). - Filter(func(v int, _ string) bool { return v > 1 }) -``` - ---- - -### Iter - -```go -func (m *MapCollection[K, V]) Iter() iter.Seq2[K, V] -``` - -Returns an `iter.Seq2[K, V]` iterator for use with Go 1.23+ range-over-func loops. Entries are yielded in insertion order. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "x", Value: 10}, - collection.Pair[string, int]{Key: "y", Value: 20}, -) - -for key, value := range m.Iter() { - fmt.Printf("%s: %d\n", key, value) -} -// Output: -// x: 10 -// y: 20 -``` - -**When to use:** Integrating with standard Go `for range` syntax while preserving insertion order. This is the idiomatic way to iterate a `MapCollection` in Go 1.23+. - ---- - -## String - -### Implode - -```go -func (m *MapCollection[K, V]) Implode(glue string) string -``` - -Concatenates all values (in insertion order) into a single string, separated by the given glue. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, string]{Key: "first", Value: "Hello"}, - collection.Pair[string, string]{Key: "second", Value: "World"}, -) -fmt.Println(m.Implode(" ")) // "Hello World" -``` - ---- - -### Join - -```go -func (m *MapCollection[K, V]) Join(glue string, finalGlues ...string) string -``` - -Like `Implode`, but with an optional final glue placed between the last two items. This is useful for producing human-readable lists. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, string]{Key: "a", Value: "Go"}, - collection.Pair[string, string]{Key: "b", Value: "Rust"}, - collection.Pair[string, string]{Key: "c", Value: "Zig"}, -) -fmt.Println(m.Join(", ", ", and ")) // "Go, Rust, and Zig" -``` - ---- - -## Conditional - -### When - -```go -func (m *MapCollection[K, V]) When( - condition bool, - callback func(*MapCollection[K, V]) *MapCollection[K, V], - defaults ...func(*MapCollection[K, V]) *MapCollection[K, V], -) *MapCollection[K, V] -``` - -Applies the callback if the condition is `true`. An optional default callback is applied when the condition is `false`. Returns the (potentially new) collection. - -```go -isAdmin := true - -m := collection.NewMap(map[string]int{"base": 100}). - When(isAdmin, func(m *collection.MapCollection[string, int]) *collection.MapCollection[string, int] { - m.Put("bonus", 50) - return m - }) -fmt.Println(m.All()) // map[base:100 bonus:50] -``` - -**When to use:** Conditionally modifying a collection in a method chain without breaking the flow with `if/else` blocks. - ---- - -### Unless - -```go -func (m *MapCollection[K, V]) Unless( - condition bool, - callback func(*MapCollection[K, V]) *MapCollection[K, V], - defaults ...func(*MapCollection[K, V]) *MapCollection[K, V], -) *MapCollection[K, V] -``` - -The inverse of `When`: applies the callback unless the condition is `true`. - -```go -isGuest := false - -m := collection.NewMap(map[string]string{"page": "dashboard"}). - Unless(isGuest, func(m *collection.MapCollection[string, string]) *collection.MapCollection[string, string] { - m.Put("menu", "full") - return m - }) -fmt.Println(m.Has("menu")) // true -``` - ---- - -## Partitioning - -### Every - -```go -func (m *MapCollection[K, V]) Every(callback func(V, K) bool) bool -``` - -Reports whether all items in the collection satisfy the callback. - -```go -m := collection.NewMap(map[string]int{"a": 2, "b": 4, "c": 6}) - -allEven := m.Every(func(v int, _ string) bool { - return v%2 == 0 -}) -fmt.Println(allEven) // true -``` - -**When to use:** Validating that every entry meets a constraint before proceeding. - ---- - -### Partition - -```go -func (m *MapCollection[K, V]) Partition( - callback func(V, K) bool, -) (*MapCollection[K, V], *MapCollection[K, V]) -``` - -Splits the collection into two: one where the callback returns `true`, and one where it returns `false`. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}) - -pass, fail := m.Partition(func(v int, _ string) bool { - return v%2 == 0 -}) -fmt.Println(pass.All()) // map[b:2 d:4] -fmt.Println(fail.All()) // map[a:1 c:3] -``` - -**When to use:** Splitting data into two groups based on a condition -- for example, separating valid from invalid entries. - ---- - -## Serialization - -### ToJSON - -```go -func (m *MapCollection[K, V]) ToJSON() ([]byte, error) -``` - -Serializes the collection to compact JSON. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2}) -data, _ := m.ToJSON() -fmt.Println(string(data)) // {"a":1,"b":2} -``` - ---- - -### ToPrettyJSON - -```go -func (m *MapCollection[K, V]) ToPrettyJSON() ([]byte, error) -``` - -Serializes the collection to indented (pretty-printed) JSON with 4-space indentation. - -```go -m := collection.NewMap(map[string]int{"a": 1, "b": 2}) -data, _ := m.ToPrettyJSON() -fmt.Println(string(data)) -// { -// "a": 1, -// "b": 2 -// } -``` - ---- - -### String - -```go -func (m *MapCollection[K, V]) String() string -``` - -Returns the JSON representation of the collection as a string. If serialization fails, returns `"{}"`. - -```go -m := collection.NewMap(map[string]int{"x": 42}) -fmt.Println(m.String()) // {"x":42} -``` - ---- - -### MarshalJSON - -```go -func (m *MapCollection[K, V]) MarshalJSON() ([]byte, error) -``` - -Implements the `json.Marshaler` interface, allowing a `MapCollection` to be used anywhere Go's `encoding/json` expects a marshaler. - -```go -m := collection.NewMap(map[string]int{"a": 1}) -data, _ := json.Marshal(m) // works because MarshalJSON is implemented -``` - ---- - -### UnmarshalJSON - -```go -func (m *MapCollection[K, V]) UnmarshalJSON(data []byte) error -``` - -Implements the `json.Unmarshaler` interface. - -```go -m := collection.NewMap(map[string]int{}) -_ = json.Unmarshal([]byte(`{"x":10,"y":20}`), m) -fmt.Println(m.All()) // map[x:10 y:20] -``` - ---- - -### Copy - -```go -func (m *MapCollection[K, V]) Copy() *MapCollection[K, V] -``` - -Creates a shallow copy of the collection. Mutations to the copy do not affect the original. - -```go -original := collection.NewMap(map[string]int{"a": 1}) -clone := original.Copy() -clone.Put("b", 2) - -fmt.Println(original.Count()) // 1 -fmt.Println(clone.Count()) // 2 -``` - ---- - -### Dump - -```go -func (m *MapCollection[K, V]) Dump() *MapCollection[K, V] -``` - -Prints the underlying map to stdout for debugging purposes. Returns the collection for chaining. - -```go -collection.NewMap(map[string]int{"debug": 42}).Dump() -// Output: map[debug:42] -``` - ---- - -### ToPairs - -```go -func (m *MapCollection[K, V]) ToPairs() *Collection[Pair[K, V]] -``` - -Converts the collection to a `Collection` of `Pair[K, V]` values, preserving insertion order. This is useful for round-tripping between `MapCollection` and pair-based representations. - -```go -m := collection.NewMapFromPairs( - collection.Pair[string, int]{Key: "a", Value: 1}, - collection.Pair[string, int]{Key: "b", Value: 2}, -) - -pairs := m.ToPairs() -fmt.Println(pairs.Count()) // 2 -// Each pair has .Key and .Value fields -``` - -**When to use:** Converting to a slice-based representation for operations that require indexing, or serializing ordered key-value data. diff --git a/docs/overview.md b/docs/overview.md index cc538ef..b898645 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,245 +1,122 @@ -# Collection -- Fluent Generic Collections for Go +# Overview -``` -go get github.com/gocanto/collection -``` - -**collection** is a Go port of Laravel's [Illuminate\Support\Collection](https://github.com/laravel/framework/tree/master/src/Illuminate/Collections). It preserves the same rich API surface -- `filter`, `map`, `reduce`, `flatMap`, `chunk`, `partition`, and many more -- while redesigning everything idiomatically for Go with generics (1.25+), `iter.Seq` for lazy evaluation, and full type safety. +A Go port of [Laravel Collections](https://laravel.com/docs/collections) β€” fluent, type-safe, and powered by Go generics and `iter.Seq` lazy evaluation. --- -## Why This Library Exists +## 🎯 Why This Library Exists Go's built-in slice and map operations are intentionally minimal. Common tasks like filtering, mapping, partitioning, and reducing require writing boilerplate loops every time. This library provides: -- **Readable data pipelines** -- chain `.Filter().Map().Take()` instead of nesting loops. -- **Type-safe generics** -- no `interface{}` casting; the compiler catches type errors at build time. -- **Immutable returns** -- methods return new collections, leaving the original untouched. -- **Lazy evaluation** -- process large or infinite datasets without allocating the entire result up front. -- **Standalone utilities** -- use individual functions from `arr` or `kv` without constructing a collection object when a single operation is all you need. +- **πŸ’Ž Readable Pipelines:** Chain `.Filter().Map().Take()` instead of nesting loops. +- **πŸ›‘οΈ Type-safe Generics:** No `interface{}` casting; the compiler catches type errors at build time. +- **♻️ Immutable Returns:** Methods return new collections, leaving the original untouched. +- **🐒 Lazy Evaluation:** Process large or infinite datasets without allocating the entire result up front. +- **🧩 Standalone Utils:** Use individual functions from `arr` or `kv` for quick, one-off operations. --- -## Package Structure +## πŸ—οΈ Package Structure -``` +```text github.com/gocanto/collection - | - |-- collection/ Core collection package - | Collection[T] Fluent wrapper around []T - | MapCollection[K,V] Ordered map with fluent API - | LazyCollection[T] Lazy sequences backed by iter.Seq[T] - | Pair[K,V] Key-value pair type - | Numeric Type constraint for numeric types - | - |-- arr/ Standalone generic slice utilities - | Flatten, Sort, Where, Map, Pluck, Partition, ... - | - |-- kv/ Map utilities with dot-notation support - Get, Set, Has, Dot, Undot, Only, Query, ... + β”œβ”€β”€ collection/ Core: Collection[T] (fluent slice wrapper) + β”œβ”€β”€ lazy/ Lazy sequences backed by iter.Seq[T] + β”œβ”€β”€ collectible/ collectible.Collection[K, V] (ordered map with fluent API) + β”œβ”€β”€ arr/ Standalone generic slice utilities + └── kv/ Map utilities with dot-notation support ``` -### Root package (`collection`) - -The root package provides three core collection types and a set of helper functions. - -### `arr` sub-package - -Package-level generic functions that operate on plain `[]T` slices. Use `arr` when you need a quick one-off operation and do not need method chaining. - -See the full API reference: [arr.md](arr.md) +### Core Packages -### `kv` sub-package - -Package-level functions for `map[string]any` (including dot-notation traversal of nested maps) and generic map utilities. - -See the full API reference: [kv.md](kv.md) +- **`collection`**: The primary workhorse for fluent slice manipulation. +- **`lazy`**: Provides deferred evaluation for large or infinite datasets. +- **`collectible`**: Handles key-value data while maintaining insertion order. --- -## Quick Start +## πŸš€ Quick Start -### Installation - -```bash -go get github.com/gocanto/collection -``` - -Requires **Go 1.25** or later. - -### Basic Collection Usage +### Basic Collection ```go -package main - -import ( - "fmt" - "github.com/gocanto/collection/collection" -) +import "github.com/gocanto/collection/collection" -func main() { - // Create a collection from a slice. - numbers := collection.Collect([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) +// Create a collection from a slice +numbers := collection.Collect([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) - // Filter to even numbers, take the first 3. - result := numbers. - Filter(func(n int, _ int) bool { return n%2 == 0 }). - Take(3) +// Filter to even numbers, take the first 3 +result := numbers. + Filter(func(n int, _ int) bool { return n % 2 == 0 }). + Take(3) - fmt.Println(result.All()) // [2, 4, 6] -} +fmt.Println(result.All()) // [2, 4, 6] ``` -### Mapping to a Different Type - -Because Go generics do not allow methods to introduce new type parameters, type-changing operations like `Map`, `Reduce`, and `FlatMap` are top-level functions: +### Map Collection ```go -names := collection.Collect([]string{"alice", "bob", "charlie"}) - -lengths := collection.Map(names, func(name string, _ int) int { - return len(name) -}) - -fmt.Println(lengths.All()) // [5, 3, 7] -``` - -### MapCollection +import "github.com/gocanto/collection/collectible" -```go -m := collection.NewMap(map[string]int{ +m := collectible.New(map[string]int{ "apples": 5, "bananas": 3, - "oranges": 8, }) -m.Has("apples") // true -val, ok := m.Get("bananas") // 3, true - -// Filter entries. -plenty := m.Filter(func(count int, _ string) bool { - return count > 4 -}) -fmt.Println(plenty.All()) // map[apples:5 oranges:8] +m.IsNotEmpty() // true ``` -### LazyCollection - -Lazy collections defer computation until results are consumed. They are backed by `iter.Seq[T]` and integrate with Go's `range`-over-function syntax: +### Lazy Collection ```go -// Generate a potentially large range lazily. -lazy := collection.LazyRange(1, 1_000_000). - Filter(func(n int, _ int) bool { return n%2 == 0 }). - Take(5) +import "github.com/gocanto/collection/lazy" -fmt.Println(lazy.All()) // [2, 4, 6, 8, 10] -// Only 10 integers were evaluated, not 1 million. -``` - -Use `range` with the iterator directly: - -```go -for val := range lazy.Iter() { - fmt.Println(val) -} -``` - -### Standalone Slice Utilities (arr) - -```go -import "github.com/gocanto/collection/arr" - -nums := []int{5, 3, 8, 1, 9, 2} - -sorted := arr.Sort(nums, func(a, b int) bool { return a < b }) -fmt.Println(sorted) // [1, 2, 3, 5, 8, 9] - -first, ok := arr.First(nums, func(v int, _ int) bool { return v > 4 }) -fmt.Println(first, ok) // 5 true -``` - -### Standalone Map Utilities (kv) - -```go -import "github.com/gocanto/collection/kv" - -config := map[string]any{ - "database": map[string]any{ - "host": "localhost", - "port": 5432, - }, -} - -host := kv.Get(config, "database.host") -fmt.Println(host) // "localhost" +// Computation is deferred until .All() or .Iter() is called +lc := lazy.Range(1, 1000000). + Filter(func(n int, _ int) bool { return n % 2 == 0 }). + Take(5) -kv.Set(config, "database.name", "mydb") +fmt.Println(lc.All()) // [2, 4, 6, 8, 10] ``` --- -## Key Design Decisions - -### Go Generics +## πŸ› οΈ Key Design Decisions -Every collection type is parameterized with type variables (`Collection[T]`, `MapCollection[K, V]`, `LazyCollection[T]`). This means: +### 1. Go Generics +Every collection type is parameterized (`Collection[T]`, `collectible.Collection[K, V]`). +- **No type assertions:** Zero runtime overhead for type checking. +- **Compile-time safety:** Errors are caught during development, not in production. -- No type assertions at call sites. -- The compiler verifies type compatibility. -- Operations like `Map` can transform `[]T` into `[]R` where `R` is a completely different type. +### 2. `iter.Seq` Integration +`lazy.Collection` is built on Go's standard iterator protocol (`iter.Seq[T]`). +- **Native Range Support:** Use `range` directly over `lc.Iter()`. +- **Deferred Execution:** Computation only happens when results are requested. -Because Go does not allow methods to introduce new type parameters, type-changing operations (`Map`, `FlatMap`, `Reduce`, `MapValues`, `Pipe`, `PipeInto`, `LazyMap`, etc.) are implemented as package-level functions rather than methods. - -### `iter.Seq` Integration - -`LazyCollection` is built on Go's `iter.Seq[T]` iterator protocol. This means: - -- Lazy collections compose naturally with any code that accepts `iter.Seq`. -- You can use `range` directly over `lc.Iter()`. -- Constructors like `NewLazy` accept any `iter.Seq[T]`, so you can wrap database cursors, file readers, or channel consumers. - -`Collection` and `MapCollection` also expose `.Iter()` methods that return `iter.Seq[T]` and `iter.Seq2[K, V]` respectively, enabling interop with the broader Go iterator ecosystem. - -### Immutable Returns - -Methods that transform data (`.Filter()`, `.Take()`, `.Reject()`, `.Flatten()`, etc.) return **new** collections. The original collection is never modified by these operations. Mutating methods like `.Push()`, `.Put()`, `.Pull()`, `.Pop()`, and `.Shift()` are clearly named and documented as in-place operations. - -This makes it safe to share collections across goroutines (for read-only access) and to build pipelines without worrying about side effects. +### 3. Immutable Returns +Methods like `.Filter()`, `.Take()`, or `.Flatten()` return **new** collections. +- **Side-effect free:** Original data remains untouched. +- **Concurrency safe:** Collections can be shared across goroutines for read-only access. --- -## When to Use Each Collection Type +## πŸ“Š Comparison: Which to Use? -| Type | Use when... | -|------|-------------| -| **`Collection[T]`** | You have a slice of data and want to filter, sort, map, chunk, or aggregate it with fluent method chaining. This is the workhorse for most day-to-day slice operations. | -| **`MapCollection[K, V]`** | You are working with key-value data and need ordered iteration, key-based lookup, filtering, merging, or set operations (diff, intersect, union). Maintains insertion order. | -| **`LazyCollection[T]`** | You are working with large datasets, streams, or infinite sequences where you do not want to materialize the entire result. Operations are deferred until you call `.All()`, `.Eager()`, `.Count()`, or iterate with `range`. | -| **`arr.FuncName`** | You need a single slice operation (one filter, one sort, one map) and do not need chaining. Avoids allocating a collection object. | -| **`kv.FuncName`** | You need to work with `map[string]any` (especially nested maps from JSON/config) using dot-notation paths, or you need a quick map filter/transform. | - -### Conversion Between Types - -```go -// Slice -> Collection -c := collection.Collect([]int{1, 2, 3}) - -// Collection -> LazyCollection (not built-in; wrap the slice) -lc := collection.LazyFrom(c.All()) - -// LazyCollection -> Collection -c2 := lc.Eager() // or lc.Collect() - -// Collection -> MapCollection (via arr.KeyBy or MapWithKeys) -users := collection.Collect([]User{ ... }) -indexed := collection.NewMap(arr.KeyBy(users.All(), func(u User) int { return u.ID })) -``` +| Type | Best for... | +|:---|:---| +| **`Collection[T]`** | Standard slice manipulation with fluent chaining. | +| **`collectible.Collection[K, V]`** | Key-value data requiring ordered iteration or set operations. | +| **`lazy.Collection[T]`** | Large datasets or streams where deferred execution is critical. | +| **`arr.FuncName`** | Single, one-off operations on raw slices. | +| **`kv.FuncName`** | Nested maps (JSON/Config) using dot-notation paths. | --- -## Further Reading +## πŸ“š Further Reading -- **[arr.md](arr.md)** -- Full API reference for the `arr` slice utilities package. -- **[kv.md](kv.md)** -- Full API reference for the `kv` map utilities package. -- **[helpers.md](helpers.md)** -- Documentation for root-package helper functions. +- [**Collection API**](collection.md) +- [**Collectible API**](collectible.md) +- [**Lazy API**](lazy.md) +- [**Array Utilities (`arr`)**](arr.md) +- [**Key-Value Utilities (`kv`)**](kv.md) +- [**Helper Functions**](helpers.md) diff --git a/go-fmt.compose.yaml b/go-fmt.compose.yaml new file mode 100644 index 0000000..14c0bed --- /dev/null +++ b/go-fmt.compose.yaml @@ -0,0 +1,9 @@ +services: + go-fmt: + image: ghcr.io/oullin/go-fmt:v0.0.14 + working_dir: /work + volumes: + - .:/work + environment: + HOST_PROJECT_PATH: ${PWD} + command: ['help'] diff --git a/kv/access.go b/kv/access.go new file mode 100644 index 0000000..3c020a7 --- /dev/null +++ b/kv/access.go @@ -0,0 +1,190 @@ +package kv + +import "strings" + +// Get retrieves a value from a nested map using dot notation. +// If the key is not found, the first default value is returned, or nil. +func Get(target map[string]any, key string, defaults ...any) any { + if key == "" { + return target + } + + segments := strings.Split(key, ".") + + var current any = target + + for _, segment := range segments { + switch v := current.(type) { + case map[string]any: + val, ok := v[segment] + + if !ok { + if len(defaults) > 0 { + return defaults[0] + } + + return nil + } + + current = val + default: + if len(defaults) > 0 { + return defaults[0] + } + + return nil + } + } + + return current +} + +// Set sets a value in a nested map using dot notation. +// By default, existing values are overwritten; pass false to preserve them. +func Set(target map[string]any, key string, value any, overwrite ...bool) map[string]any { + shouldOverwrite := true + + if len(overwrite) > 0 { + shouldOverwrite = overwrite[0] + } + + segments := strings.Split(key, ".") + current := target + + for i, segment := range segments { + if i == len(segments)-1 { + if shouldOverwrite { + current[segment] = value + } else { + if _, exists := current[segment]; !exists { + current[segment] = value + } + } + } else { + if _, exists := current[segment]; !exists { + current[segment] = make(map[string]any) + } + + if next, ok := current[segment].(map[string]any); ok { + current = next + } else { + newMap := make(map[string]any) + current[segment] = newMap + current = newMap + } + } + } + + return target +} + +// Has reports whether the given dot-notated key exists in the nested map. +func Has(target map[string]any, key string) bool { + if key == "" { + return false + } + + segments := strings.Split(key, ".") + + var current any = target + + for _, segment := range segments { + switch v := current.(type) { + case map[string]any: + val, ok := v[segment] + + if !ok { + return false + } + + current = val + default: + return false + } + } + + return true +} + +// Fill sets the value at the given dot-notated key only if it does not already exist. +func Fill(target map[string]any, key string, value any) map[string]any { + return Set(target, key, value, false) +} + +// Add is an alias for [Fill]. It sets a value only if the key does not already exist. +func Add(target map[string]any, key string, value any) map[string]any { + return Fill(target, key, value) +} + +// Forget removes the value at the given dot-notated key from the nested map. +func Forget(target map[string]any, key string) map[string]any { + segments := strings.Split(key, ".") + + if len(segments) == 1 { + delete(target, key) + + return target + } + + current := target + + for i := 0; i < len(segments)-1; i++ { + val, ok := current[segments[i]] + + if !ok { + return target + } + + next, ok := val.(map[string]any) + + if !ok { + return target + } + + current = next + } + + delete(current, segments[len(segments)-1]) + + return target +} + +// ForgetMany removes one or more dot-notated keys from the map. +func ForgetMany(items map[string]any, keys ...string) map[string]any { + for _, key := range keys { + Forget(items, key) + } + + return items +} + +// Pull removes a key from the map and returns its value. +// If the key does not exist, the first default value is returned. +func Pull(items map[string]any, key string, defaults ...any) any { + value := Get(items, key, defaults...) + Forget(items, key) + + return value +} + +// HasAll reports whether all the given dot-notated keys exist in the map. +func HasAll(items map[string]any, keys ...string) bool { + for _, key := range keys { + if !Has(items, key) { + return false + } + } + + return true +} + +// HasAny reports whether at least one of the given dot-notated keys exists in the map. +func HasAny(items map[string]any, keys ...string) bool { + for _, key := range keys { + if Has(items, key) { + return true + } + } + + return false +} diff --git a/kv/encoding.go b/kv/encoding.go new file mode 100644 index 0000000..df81c8e --- /dev/null +++ b/kv/encoding.go @@ -0,0 +1,56 @@ +package kv + +import ( + "fmt" + "net/url" + "sort" + "strings" +) + +// Query encodes the map as a URL query string. +func Query(items map[string]any) string { + params := url.Values{} + + for k, v := range items { + params.Set(k, fmt.Sprint(v)) + } + + return params.Encode() +} + +// ToCssClasses returns a space-separated string of CSS class names +// whose corresponding boolean values are true. Classes are sorted alphabetically. +func ToCssClasses(classes map[string]bool) string { + result := make([]string, 0) + + for class, condition := range classes { + if condition { + result = append(result, class) + } + } + + sort.Strings(result) + + return strings.Join(result, " ") +} + +// ToCssStyles returns a space-separated string of CSS style declarations +// whose corresponding boolean values are true. A trailing semicolon is appended +// to each style if not already present. Styles are sorted alphabetically. +func ToCssStyles(styles map[string]bool) string { + result := make([]string, 0) + + for style, condition := range styles { + if condition { + if !strings.HasSuffix(style, ";") { + style += ";" + } + + result = append(result, style) + } + } + + sort.Strings(result) + + return strings.Join(result, " ") +} diff --git a/kv/filter.go b/kv/filter.go new file mode 100644 index 0000000..0bb011c --- /dev/null +++ b/kv/filter.go @@ -0,0 +1,34 @@ +package kv + +// Only returns a new map containing only the specified keys. +func Only(items map[string]any, keys ...string) map[string]any { + result := make(map[string]any) + + for _, key := range keys { + if v, ok := items[key]; ok { + result[key] = v + } + } + + return result +} + +// Except returns a new map with the specified keys removed. +func Except(items map[string]any, keys ...string) map[string]any { + result := make(map[string]any, len(items)) + + for k, v := range items { + result[k] = v + } + + for _, key := range keys { + delete(result, key) + } + + return result +} + +// IsAssoc reports whether the map is non-empty. +func IsAssoc(items map[string]any) bool { + return len(items) > 0 +} diff --git a/kv/kv.go b/kv/kv.go deleted file mode 100644 index afaf5e9..0000000 --- a/kv/kv.go +++ /dev/null @@ -1,370 +0,0 @@ -package kv - -import ( - "fmt" - "net/url" - "sort" - "strings" -) - -// --- Dot-notation helpers for nested map[string]any --- - -// Get retrieves a value from a nested map using dot notation. -// If the key is not found, the first default value is returned, or nil. -func Get(target map[string]any, key string, defaults ...any) any { - if key == "" { - return target - } - - segments := strings.Split(key, ".") - var current any = target - - for _, segment := range segments { - switch v := current.(type) { - case map[string]any: - val, ok := v[segment] - if !ok { - if len(defaults) > 0 { - return defaults[0] - } - return nil - } - current = val - default: - if len(defaults) > 0 { - return defaults[0] - } - return nil - } - } - - return current -} - -// Set sets a value in a nested map using dot notation. -// By default existing values are overwritten; pass false to preserve them. -func Set(target map[string]any, key string, value any, overwrite ...bool) map[string]any { - shouldOverwrite := true - if len(overwrite) > 0 { - shouldOverwrite = overwrite[0] - } - - segments := strings.Split(key, ".") - current := target - - for i, segment := range segments { - if i == len(segments)-1 { - if shouldOverwrite { - current[segment] = value - } else { - if _, exists := current[segment]; !exists { - current[segment] = value - } - } - } else { - if _, exists := current[segment]; !exists { - current[segment] = make(map[string]any) - } - if next, ok := current[segment].(map[string]any); ok { - current = next - } else { - newMap := make(map[string]any) - current[segment] = newMap - current = newMap - } - } - } - - return target -} - -// Has reports whether the given dot-notated key exists in the nested map. -func Has(target map[string]any, key string) bool { - if key == "" { - return false - } - - segments := strings.Split(key, ".") - var current any = target - - for _, segment := range segments { - switch v := current.(type) { - case map[string]any: - val, ok := v[segment] - if !ok { - return false - } - current = val - default: - return false - } - } - - return true -} - -// Fill sets the value at the given dot-notated key only if it does not already exist. -func Fill(target map[string]any, key string, value any) map[string]any { - return Set(target, key, value, false) -} - -// Add is an alias for [Fill]. It sets a value only if the key does not already exist. -func Add(target map[string]any, key string, value any) map[string]any { - return Fill(target, key, value) -} - -// Forget removes the value at the given dot-notated key from the nested map. -func Forget(target map[string]any, key string) map[string]any { - segments := strings.Split(key, ".") - - if len(segments) == 1 { - delete(target, key) - return target - } - - current := target - for i := 0; i < len(segments)-1; i++ { - val, ok := current[segments[i]] - if !ok { - return target - } - next, ok := val.(map[string]any) - if !ok { - return target - } - current = next - } - - delete(current, segments[len(segments)-1]) - return target -} - -// ForgetMany removes one or more dot-notated keys from the map. -func ForgetMany(items map[string]any, keys ...string) map[string]any { - for _, key := range keys { - Forget(items, key) - } - return items -} - -// Pull removes a key from the map and returns its value. -// If the key does not exist, the first default value is returned. -func Pull(items map[string]any, key string, defaults ...any) any { - value := Get(items, key, defaults...) - Forget(items, key) - return value -} - -// HasAll reports whether all of the given dot-notated keys exist in the map. -func HasAll(items map[string]any, keys ...string) bool { - for _, key := range keys { - if !Has(items, key) { - return false - } - } - return true -} - -// HasAny reports whether at least one of the given dot-notated keys exists in the map. -func HasAny(items map[string]any, keys ...string) bool { - for _, key := range keys { - if Has(items, key) { - return true - } - } - return false -} - -// --- Dot / Undot --- - -// Dot flattens a nested map into a single-level map with dot-notated keys. -// An optional prefix is prepended to each key. -func Dot(items map[string]any, prepend ...string) map[string]any { - prefix := "" - if len(prepend) > 0 { - prefix = prepend[0] - } - result := make(map[string]any) - dotRecursive(items, prefix, result) - return result -} - -func dotRecursive(items map[string]any, prefix string, result map[string]any) { - for key, value := range items { - fullKey := key - if prefix != "" { - fullKey = prefix + "." + key - } - if nested, ok := value.(map[string]any); ok { - dotRecursive(nested, fullKey, result) - } else { - result[fullKey] = value - } - } -} - -// Undot expands a flat map with dot-notated keys into a nested map. -func Undot(items map[string]any) map[string]any { - result := make(map[string]any) - for key, value := range items { - Set(result, key, value) - } - return result -} - -// --- Map filtering and selection --- - -// Only returns a new map containing only the specified keys. -func Only(items map[string]any, keys ...string) map[string]any { - result := make(map[string]any) - for _, key := range keys { - if v, ok := items[key]; ok { - result[key] = v - } - } - return result -} - -// Except returns a new map with the specified keys removed. -func Except(items map[string]any, keys ...string) map[string]any { - result := make(map[string]any, len(items)) - for k, v := range items { - result[k] = v - } - for _, key := range keys { - delete(result, key) - } - return result -} - -// IsAssoc reports whether the map is non-empty. -func IsAssoc(items map[string]any) bool { - return len(items) > 0 -} - -// --- Encoding --- - -// Query encodes the map as a URL query string. -func Query(items map[string]any) string { - params := url.Values{} - for k, v := range items { - params.Set(k, fmt.Sprint(v)) - } - return params.Encode() -} - -// ToCssClasses returns a space-separated string of CSS class names -// whose corresponding boolean values are true. Classes are sorted alphabetically. -func ToCssClasses(classes map[string]bool) string { - result := make([]string, 0) - for class, condition := range classes { - if condition { - result = append(result, class) - } - } - sort.Strings(result) - return strings.Join(result, " ") -} - -// ToCssStyles returns a space-separated string of CSS style declarations -// whose corresponding boolean values are true. A trailing semicolon is appended -// to each style if not already present. Styles are sorted alphabetically. -func ToCssStyles(styles map[string]bool) string { - result := make([]string, 0) - for style, condition := range styles { - if condition { - if !strings.HasSuffix(style, ";") { - style += ";" - } - result = append(result, style) - } - } - sort.Strings(result) - return strings.Join(result, " ") -} - -// --- Sorting --- - -// Sort returns a new map with the same entries, iterated by sorted keys. -// Note: Go maps do not guarantee iteration order; the returned map -// is sorted at construction time. -func Sort(items map[string]any) map[string]any { - keys := make([]string, 0, len(items)) - for k := range items { - keys = append(keys, k) - } - sort.Strings(keys) - result := make(map[string]any, len(items)) - for _, k := range keys { - result[k] = items[k] - } - return result -} - -// SortRecursive returns a new map with entries sorted by key, -// recursing into any nested maps. -func SortRecursive(items map[string]any) map[string]any { - result := make(map[string]any, len(items)) - keys := make([]string, 0, len(items)) - for k := range items { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - v := items[k] - if nested, ok := v.(map[string]any); ok { - result[k] = SortRecursive(nested) - } else { - result[k] = v - } - } - return result -} - -// --- Map transformation --- - -// Map applies a callback to each value in the map and returns a new map -// with the transformed values. -func Map[V any, R any](items map[string]V, callback func(V, string) R) map[string]R { - result := make(map[string]R, len(items)) - for k, v := range items { - result[k] = callback(v, k) - } - return result -} - -// Where returns a new map containing only the entries for which -// the callback returns true. -func Where[V any](items map[string]V, callback func(V, string) bool) map[string]V { - result := make(map[string]V) - for k, v := range items { - if callback(v, k) { - result[k] = v - } - } - return result -} - -// PrependKeysWith returns a new map with the given prefix prepended to every key. -func PrependKeysWith[V any](items map[string]V, prefix string) map[string]V { - result := make(map[string]V, len(items)) - for k, v := range items { - result[prefix+k] = v - } - return result -} - -// Replace returns a new map with entries from items, overwritten by -// any matching keys from the replacement maps. -func Replace(items map[string]any, replacements ...map[string]any) map[string]any { - result := make(map[string]any, len(items)) - for k, v := range items { - result[k] = v - } - for _, replacement := range replacements { - for k, v := range replacement { - result[k] = v - } - } - return result -} diff --git a/kv/kv_test.go b/kv/kv_test.go index d8042aa..2d833fe 100644 --- a/kv/kv_test.go +++ b/kv/kv_test.go @@ -15,21 +15,25 @@ func TestGet(t *testing.T) { }, } v := kv.Get(data, "user.name") + if v != "Alice" { t.Errorf("expected Alice, got %v", v) } v = kv.Get(data, "user.email", "default@test.com") + if v != "default@test.com" { t.Errorf("expected default, got %v", v) } v = kv.Get(data, "missing.key") + if v != nil { t.Errorf("expected nil, got %v", v) } v = kv.Get(data, "") + if v == nil { t.Error("expected non-nil for empty key") } @@ -40,6 +44,7 @@ func TestSet(t *testing.T) { kv.Set(data, "user.name", "Alice") v := kv.Get(data, "user.name") + if v != "Alice" { t.Errorf("expected Alice, got %v", v) } @@ -48,6 +53,7 @@ func TestSet(t *testing.T) { func TestSetNoOverwrite(t *testing.T) { data := map[string]any{"name": "Alice"} kv.Set(data, "name", "Bob", false) + if data["name"] != "Alice" { t.Errorf("expected Alice (not overwritten), got %v", data["name"]) } @@ -63,9 +69,11 @@ func TestHas(t *testing.T) { if !kv.Has(data, "user.name") { t.Error("expected true") } + if kv.Has(data, "user.email") { t.Error("expected false") } + if kv.Has(data, "") { t.Error("expected false for empty key") } @@ -77,11 +85,13 @@ func TestFill(t *testing.T) { } kv.Fill(data, "name", "Bob") // Should not overwrite + if data["name"] != "Alice" { t.Errorf("expected Alice (not overwritten), got %v", data["name"]) } kv.Fill(data, "email", "alice@test.com") // Should fill + if data["email"] != "alice@test.com" { t.Errorf("expected email to be filled, got %v", data["email"]) } @@ -90,10 +100,13 @@ func TestFill(t *testing.T) { func TestAdd(t *testing.T) { data := map[string]any{"name": "Alice"} kv.Add(data, "name", "Bob") + if data["name"] != "Alice" { t.Errorf("expected Alice, got %v", data["name"]) } + kv.Add(data, "email", "alice@test.com") + if data["email"] != "alice@test.com" { t.Errorf("expected email set, got %v", data["email"]) } @@ -108,9 +121,11 @@ func TestForget(t *testing.T) { } kv.Forget(data, "user.email") + if kv.Has(data, "user.email") { t.Error("expected email to be removed") } + if !kv.Has(data, "user.name") { t.Error("expected name to still exist") } @@ -124,6 +139,7 @@ func TestForgetMany(t *testing.T) { }, } kv.ForgetMany(data, "user.email", "user.name") + if kv.Has(data, "user.email") || kv.Has(data, "user.name") { t.Error("expected both keys removed") } @@ -132,9 +148,11 @@ func TestForgetMany(t *testing.T) { func TestPull(t *testing.T) { data := map[string]any{"name": "Alice", "age": 25} v := kv.Pull(data, "name") + if v != "Alice" { t.Errorf("expected Alice, got %v", v) } + if _, ok := data["name"]; ok { t.Error("expected name to be removed") } @@ -146,9 +164,11 @@ func TestHasAll(t *testing.T) { "name": "Alice", }, } + if !kv.HasAll(data, "user.name") { t.Error("expected true") } + if kv.HasAll(data, "user.name", "user.email") { t.Error("expected false when one key missing") } @@ -158,9 +178,11 @@ func TestHasAny(t *testing.T) { data := map[string]any{ "name": "Alice", } + if !kv.HasAny(data, "email", "name") { t.Error("expected true") } + if kv.HasAny(data, "email", "phone") { t.Error("expected false") } @@ -175,9 +197,11 @@ func TestDot(t *testing.T) { "status": "active", } dotted := kv.Dot(data) + if dotted["user.name"] != "Alice" { t.Errorf("expected Alice, got %v", dotted["user.name"]) } + if dotted["status"] != "active" { t.Errorf("expected active, got %v", dotted["status"]) } @@ -188,6 +212,7 @@ func TestDotWithPrefix(t *testing.T) { "name": "Alice", } result := kv.Dot(data, "user") + if result["user.name"] != "Alice" { t.Errorf("expected Alice at 'user.name', got %v", result["user.name"]) } @@ -200,6 +225,7 @@ func TestUndot(t *testing.T) { } undotted := kv.Undot(dotted) v := kv.Get(undotted, "user.name") + if v != "Alice" { t.Errorf("expected Alice, got %v", v) } @@ -208,9 +234,11 @@ func TestUndot(t *testing.T) { func TestOnly(t *testing.T) { data := map[string]any{"a": 1, "b": 2, "c": 3} result := kv.Only(data, "a", "c") + if len(result) != 2 { t.Errorf("expected 2, got %d", len(result)) } + if result["a"] != 1 { t.Error("expected a=1") } @@ -219,9 +247,11 @@ func TestOnly(t *testing.T) { func TestExcept(t *testing.T) { data := map[string]any{"a": 1, "b": 2, "c": 3} result := kv.Except(data, "b") + if len(result) != 2 { t.Errorf("expected 2, got %d", len(result)) } + if _, ok := result["b"]; ok { t.Error("expected b to be excluded") } @@ -231,6 +261,7 @@ func TestIsAssoc(t *testing.T) { if !kv.IsAssoc(map[string]any{"a": 1}) { t.Error("expected true") } + if kv.IsAssoc(map[string]any{}) { t.Error("expected false for empty map") } @@ -239,6 +270,7 @@ func TestIsAssoc(t *testing.T) { func TestQuery(t *testing.T) { data := map[string]any{"name": "Alice", "age": 25} result := kv.Query(data) + if !strings.Contains(result, "name=Alice") { t.Errorf("expected name=Alice in query, got %s", result) } @@ -251,9 +283,11 @@ func TestToCssClasses(t *testing.T) { "visible": true, } result := kv.ToCssClasses(classes) + if !strings.Contains(result, "active") { t.Error("expected 'active' in result") } + if strings.Contains(result, "disabled") { t.Error("expected 'disabled' to be excluded") } @@ -266,6 +300,7 @@ func TestToCssStyles(t *testing.T) { "font-weight: bold": true, } result := kv.ToCssStyles(styles) + if !strings.Contains(result, "color: red;") { t.Errorf("expected 'color: red;' in result, got '%s'", result) } @@ -274,6 +309,7 @@ func TestToCssStyles(t *testing.T) { func TestSort(t *testing.T) { data := map[string]any{"c": 3, "a": 1, "b": 2} result := kv.Sort(data) + if len(result) != 3 { t.Errorf("expected 3 items, got %d", len(result)) } @@ -288,19 +324,24 @@ func TestSortRecursive(t *testing.T) { }, } result := kv.SortRecursive(data) + if result == nil { t.Error("expected non-nil result") } + nested, ok := result["a"].(map[string]any) + if !ok { t.Error("expected nested map") } + _ = nested } func TestMap(t *testing.T) { data := map[string]int{"a": 1, "b": 2} result := kv.Map(data, func(v int, k string) int { return v * 10 }) + if result["a"] != 10 { t.Errorf("expected 10, got %d", result["a"]) } @@ -309,6 +350,7 @@ func TestMap(t *testing.T) { func TestWhere(t *testing.T) { data := map[string]int{"a": 1, "b": 2, "c": 3} result := kv.Where(data, func(v int, k string) bool { return v > 1 }) + if len(result) != 2 { t.Errorf("expected 2, got %d", len(result)) } @@ -317,6 +359,7 @@ func TestWhere(t *testing.T) { func TestPrependKeysWith(t *testing.T) { data := map[string]int{"name": 1, "age": 2} result := kv.PrependKeysWith(data, "user_") + if _, ok := result["user_name"]; !ok { t.Error("expected 'user_name' key") } @@ -325,9 +368,11 @@ func TestPrependKeysWith(t *testing.T) { func TestReplace(t *testing.T) { data := map[string]any{"a": 1, "b": 2} result := kv.Replace(data, map[string]any{"b": 20, "c": 3}) + if result["b"] != 20 { t.Errorf("expected 20, got %v", result["b"]) } + if result["c"] != 3 { t.Errorf("expected 3, got %v", result["c"]) } @@ -338,12 +383,14 @@ func TestMapTransformTypes(t *testing.T) { ID int Name string } + data := map[string]User{ "a": {1, "Alice"}, "b": {2, "Bob"}, } result := kv.Map(data, func(u User, _ string) string { return u.Name }) expected := map[string]string{"a": "Alice", "b": "Bob"} + if !reflect.DeepEqual(result, expected) { t.Errorf("expected %v, got %v", expected, result) } diff --git a/kv/sorting.go b/kv/sorting.go new file mode 100644 index 0000000..e6227a9 --- /dev/null +++ b/kv/sorting.go @@ -0,0 +1,49 @@ +package kv + +import "sort" + +// Sort returns a new map with the same entries, iterated by sorted keys. +// Note: Go maps do not guarantee iteration order; the returned map +// is sorted at construction time. +func Sort(items map[string]any) map[string]any { + keys := make([]string, 0, len(items)) + + for k := range items { + keys = append(keys, k) + } + + sort.Strings(keys) + + result := make(map[string]any, len(items)) + + for _, k := range keys { + result[k] = items[k] + } + + return result +} + +// SortRecursive returns a new map with entries sorted by key, +// recursing into any nested maps. +func SortRecursive(items map[string]any) map[string]any { + result := make(map[string]any, len(items)) + keys := make([]string, 0, len(items)) + + for k := range items { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + v := items[k] + + if nested, ok := v.(map[string]any); ok { + result[k] = SortRecursive(nested) + } else { + result[k] = v + } + } + + return result +} diff --git a/kv/transform.go b/kv/transform.go new file mode 100644 index 0000000..b4cc854 --- /dev/null +++ b/kv/transform.go @@ -0,0 +1,98 @@ +package kv + +// Dot flattens a nested map into a single-level map with dot-notated keys. +// An optional prefix is prepended to each key. +func Dot(items map[string]any, prepend ...string) map[string]any { + prefix := "" + + if len(prepend) > 0 { + prefix = prepend[0] + } + + result := make(map[string]any) + dotRecursive(items, prefix, result) + + return result +} + +func dotRecursive(items map[string]any, prefix string, result map[string]any) { + for key, value := range items { + fullKey := key + + if prefix != "" { + fullKey = prefix + "." + key + } + + if nested, ok := value.(map[string]any); ok { + dotRecursive(nested, fullKey, result) + } else { + result[fullKey] = value + } + } +} + +// Undot expands a flat map with dot-notated keys into a nested map. +func Undot(items map[string]any) map[string]any { + result := make(map[string]any) + + for key, value := range items { + Set(result, key, value) + } + + return result +} + +// Map applies a callback to each value in the map and returns a new map +// with the transformed values. +func Map[V any, R any](items map[string]V, callback func(V, string) R) map[string]R { + result := make(map[string]R, len(items)) + + for k, v := range items { + result[k] = callback(v, k) + } + + return result +} + +// Where returns a new map containing only the entries for which +// the callback returns true. +func Where[V any](items map[string]V, callback func(V, string) bool) map[string]V { + result := make(map[string]V) + + for k, v := range items { + if callback(v, k) { + result[k] = v + } + } + + return result +} + +// PrependKeysWith returns a new map with the given prefix prepended to every key. +func PrependKeysWith[V any](items map[string]V, prefix string) map[string]V { + result := make(map[string]V, len(items)) + + for k, v := range items { + result[prefix+k] = v + } + + return result +} + +// Replace returns a new map with entries from items, overwritten by +// any matching keys from the replacement maps. +func Replace(items map[string]any, replacements ...map[string]any) map[string]any { + result := make(map[string]any, len(items)) + + for k, v := range items { + result[k] = v + } + + for _, replacement := range replacements { + for k, v := range replacement { + result[k] = v + } + } + + return result +} diff --git a/lazy/access.go b/lazy/access.go new file mode 100644 index 0000000..b21474a --- /dev/null +++ b/lazy/access.go @@ -0,0 +1,310 @@ +package lazy + +import "github.com/gocanto/collection/support" + +// First returns the first element matching the optional predicate. +// If no predicate is given, it returns the first element. +// The second return value indicates whether a matching element was found. +func (lc *Collection[T]) First(predicates ...func(T, int) bool) (T, bool) { + var result T + found := false + idx := 0 + + if len(predicates) == 0 || predicates[0] == nil { + lc.source(func(item T) bool { + result = item + found = true + + return false + }) + } else { + predicate := predicates[0] + lc.source(func(item T) bool { + if predicate(item, idx) { + result = item + found = true + + return false + } + + idx++ + + return true + }) + } + + return result, found +} + +// FirstOrFail returns the first element matching the optional predicate, +// or an ItemNotFoundError if no element is found. +func (lc *Collection[T]) FirstOrFail(predicates ...func(T, int) bool) (T, error) { + item, ok := lc.First(predicates...) + + if !ok { + var zero T + + return zero, &support.ItemNotFoundError{} + } + + return item, nil +} + +// Last returns the last element matching the optional predicate. +// The second return value indicates whether a matching element was found. +func (lc *Collection[T]) Last(predicates ...func(T, int) bool) (T, bool) { + var result T + found := false + idx := 0 + + if len(predicates) == 0 || predicates[0] == nil { + lc.source(func(item T) bool { + result = item + found = true + + return true + }) + } else { + predicate := predicates[0] + lc.source(func(item T) bool { + if predicate(item, idx) { + result = item + found = true + } + + idx++ + + return true + }) + } + + return result, found +} + +// Sole returns the only element matching the optional predicate. +// It returns an error if zero or more than one element matches. +func (lc *Collection[T]) Sole(predicates ...func(T, int) bool) (T, error) { + var result T + count := 0 + idx := 0 + + if len(predicates) == 0 || predicates[0] == nil { + lc.source(func(item T) bool { + result = item + count++ + + return count < 2 + }) + } else { + predicate := predicates[0] + lc.source(func(item T) bool { + if predicate(item, idx) { + result = item + count++ + + if count > 1 { + return false + } + } + + idx++ + + return true + }) + } + + if count == 0 { + var zero T + + return zero, &support.ItemNotFoundError{} + } + + if count > 1 { + var zero T + + return zero, &support.MultipleItemsFoundError{Count: count} + } + + return result, nil +} + +// Get returns the item at the given zero-based index. +// The second return value indicates whether the index exists. +func (lc *Collection[T]) Get(index int) (T, bool) { + var result T + found := false + idx := 0 + lc.source(func(item T) bool { + if idx == index { + result = item + found = true + + return false + } + + idx++ + + return true + }) + + return result, found +} + +// Contains reports whether any item satisfies the predicate. +func (lc *Collection[T]) Contains(predicate func(T, int) bool) bool { + found := false + idx := 0 + lc.source(func(item T) bool { + if predicate(item, idx) { + found = true + + return false + } + + idx++ + + return true + }) + + return found +} + +// Some is an alias for Contains. +func (lc *Collection[T]) Some(predicate func(T, int) bool) bool { + return lc.Contains(predicate) +} + +// DoesntContain reports whether no item satisfies the predicate. +func (lc *Collection[T]) DoesntContain(predicate func(T, int) bool) bool { + return !lc.Contains(predicate) +} + +// Search returns the index of the first item satisfying the predicate. +// The second return value indicates whether a match was found. +func (lc *Collection[T]) Search(predicate func(T, int) bool) (int, bool) { + resultIdx := -1 + found := false + idx := 0 + lc.source(func(item T) bool { + if predicate(item, idx) { + resultIdx = idx + found = true + + return false + } + + idx++ + + return true + }) + + return resultIdx, found +} + +// Before returns the item immediately before the first item matching the predicate. +// The second return value indicates whether such an item exists. +func (lc *Collection[T]) Before(predicate func(T, int) bool) (T, bool) { + var prev T + hasPrev := false + found := false + idx := 0 + lc.source(func(item T) bool { + if predicate(item, idx) { + found = true + + return false + } + + prev = item + hasPrev = true + idx++ + + return true + }) + + if found && hasPrev { + return prev, true + } + + var zero T + + return zero, false +} + +// After returns the item immediately after the first item matching the predicate. +// The second return value indicates whether such an item exists. +func (lc *Collection[T]) After(predicate func(T, int) bool) (T, bool) { + matched := false + + var result T + found := false + idx := 0 + lc.source(func(item T) bool { + if matched { + result = item + found = true + + return false + } + + if predicate(item, idx) { + matched = true + } + + idx++ + + return true + }) + + return result, found +} + +// Has reports whether the given zero-based index exists in the collection. +func (lc *Collection[T]) Has(index int) bool { + _, ok := lc.Get(index) + + return ok +} + +// HasAny reports whether any of the given indices exist in the collection. +func (lc *Collection[T]) HasAny(indices ...int) bool { + for _, idx := range indices { + if lc.Has(idx) { + return true + } + } + + return false +} + +// HasSole reports whether exactly one item matches the optional predicate. +// If no predicate is given, it checks whether the collection has exactly one item. +func (lc *Collection[T]) HasSole(predicates ...func(T, int) bool) bool { + count := 0 + idx := 0 + + if len(predicates) == 0 || predicates[0] == nil { + lc.source(func(_ T) bool { + count++ + + return count < 2 + }) + } else { + predicate := predicates[0] + lc.source(func(item T) bool { + if predicate(item, idx) { + count++ + + if count > 1 { + return false + } + } + + idx++ + + return true + }) + } + + return count == 1 +} diff --git a/lazy/aggregate.go b/lazy/aggregate.go new file mode 100644 index 0000000..ede6bd7 --- /dev/null +++ b/lazy/aggregate.go @@ -0,0 +1,16 @@ +package lazy + +// Reduce reduces the lazy collection to a single value by applying the callback +// to an accumulator and each item in sequence. +func Reduce[T any, R any](lc *Collection[T], callback func(R, T, int) R, initial R) R { + result := initial + idx := 0 + lc.source(func(item T) bool { + result = callback(result, item, idx) + idx++ + + return true + }) + + return result +} diff --git a/lazy/collection.go b/lazy/collection.go new file mode 100644 index 0000000..4b89989 --- /dev/null +++ b/lazy/collection.go @@ -0,0 +1,130 @@ +package lazy + +import "iter" + +// Collection provides lazy evaluation for collection operations. +// Items are only computed when needed, making it memory-efficient for large datasets. +type Collection[T any] struct { + source iter.Seq[T] +} + +// New creates a new Collection from an iterator function. +// The source receives a yield function; call yield for each item and +// stop generating if the yield returns false. +func New[T any](source iter.Seq[T]) *Collection[T] { + return &Collection[T]{source: source} +} + +// From creates a Collection from a slice. +func From[T any](items []T) *Collection[T] { + return New(func(yield func(T) bool) { + for _, item := range items { + if !yield(item) { + return + } + } + }) +} + +// Empty creates an empty Collection. +func Empty[T any]() *Collection[T] { + return New[T](func(yield func(T) bool) {}) +} + +// Range creates a lazy collection of sequential integers from start to end (inclusive). +// If from > to, the sequence counts downward. +func Range(from, to int) *Collection[int] { + return New(func(yield func(int) bool) { + if from <= to { + for i := from; i <= to; i++ { + if !yield(i) { + return + } + } + } else { + for i := from; i >= to; i-- { + if !yield(i) { + return + } + } + } + }) +} + +// Times create a lazy collection by invoking the callback n times. +// The callback receives a 1-based index. +func Times[T any](number int, callback func(int) T) *Collection[T] { + return New(func(yield func(T) bool) { + for i := 1; i <= number; i++ { + if !yield(callback(i)) { + return + } + } + }) +} + +// Iter returns the underlying iterator for use with range loops. +func (lc *Collection[T]) Iter() iter.Seq[T] { + return lc.source +} + +// All eagerly evaluates the lazy collection and returns all items as a slice. +func (lc *Collection[T]) All() []T { + result := make([]T, 0) + lc.source(func(item T) bool { + result = append(result, item) + + return true + }) + + return result +} + +// Eager eagerly evaluates the lazy collection and returns all items as a slice. +func (lc *Collection[T]) Eager() []T { + return lc.All() +} + +// Collect converts the lazy collection to a slice. +func (lc *Collection[T]) Collect() []T { + return lc.All() +} + +// Count returns the total number of items by eagerly evaluating the collection. +func (lc *Collection[T]) Count() int { + count := 0 + lc.source(func(_ T) bool { + count++ + + return true + }) + + return count +} + +// IsEmpty reports whether the lazy collection contains no items. +func (lc *Collection[T]) IsEmpty() bool { + empty := true + lc.source(func(_ T) bool { + empty = false + + return false + }) + + return empty +} + +// IsNotEmpty reports whether the lazy collection contains at least one item. +func (lc *Collection[T]) IsNotEmpty() bool { + return !lc.IsEmpty() +} + +// ContainsOneItem reports whether the lazy collection contains exactly one item. +func (lc *Collection[T]) ContainsOneItem() bool { + return lc.Count() == 1 +} + +// ContainsManyItems reports whether the lazy collection contains more than one item. +func (lc *Collection[T]) ContainsManyItems() bool { + return lc.Count() > 1 +} diff --git a/lazy/collection_test.go b/lazy/collection_test.go new file mode 100644 index 0000000..451c75c --- /dev/null +++ b/lazy/collection_test.go @@ -0,0 +1,904 @@ +package lazy + +import ( + "reflect" + "testing" + "time" +) + +func TestFrom(t *testing.T) { + lc := From([]int{1, 2, 3}) + items := lc.All() + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(items, expected) { + t.Errorf("expected %v, got %v", expected, items) + } +} + +func TestEmpty(t *testing.T) { + lc := Empty[int]() + + if !lc.IsEmpty() { + t.Error("expected empty") + } + + if lc.IsNotEmpty() { + t.Error("expected empty") + } +} + +func TestRange(t *testing.T) { + lc := Range(1, 5) + items := lc.All() + expected := []int{1, 2, 3, 4, 5} + + if !reflect.DeepEqual(items, expected) { + t.Errorf("expected %v, got %v", expected, items) + } + + lc2 := Range(5, 1) + items2 := lc2.All() + expected2 := []int{5, 4, 3, 2, 1} + + if !reflect.DeepEqual(items2, expected2) { + t.Errorf("expected %v, got %v", expected2, items2) + } +} + +func TestTimes(t *testing.T) { + lc := Times(3, func(i int) int { return i * 10 }) + expected := []int{10, 20, 30} + + if !reflect.DeepEqual(lc.All(), expected) { + t.Errorf("expected %v, got %v", expected, lc.All()) + } +} + +func TestCount(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + + if lc.Count() != 5 { + t.Errorf("expected 5, got %d", lc.Count()) + } +} + +func TestEager(t *testing.T) { + lc := From([]int{1, 2, 3}) + items := lc.Eager() + + if len(items) != 3 { + t.Errorf("expected 3, got %d", len(items)) + } +} + +func TestFirst(t *testing.T) { + lc := From([]int{10, 20, 30}) + v, ok := lc.First() + + if !ok || v != 10 { + t.Errorf("expected 10, got %d", v) + } + + v, ok = lc.First(func(item int, _ int) bool { return item > 15 }) + + if !ok || v != 20 { + t.Errorf("expected 20, got %d", v) + } +} + +func TestLast(t *testing.T) { + lc := From([]int{10, 20, 30}) + v, ok := lc.Last() + + if !ok || v != 30 { + t.Errorf("expected 30, got %d", v) + } + + v, ok = lc.Last(func(item int, _ int) bool { return item < 25 }) + + if !ok || v != 20 { + t.Errorf("expected 20, got %d", v) + } +} + +func TestGet(t *testing.T) { + lc := From([]int{10, 20, 30}) + v, ok := lc.Get(1) + + if !ok || v != 20 { + t.Errorf("expected 20, got %d", v) + } + + _, ok = lc.Get(10) + + if ok { + t.Error("expected not found") + } +} + +func TestContains(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + + if !lc.Contains(func(item int, _ int) bool { return item == 3 }) { + t.Error("expected to contain 3") + } + + if lc.Contains(func(item int, _ int) bool { return item == 10 }) { + t.Error("expected not to contain 10") + } +} + +func TestSearch(t *testing.T) { + lc := From([]int{10, 20, 30}) + idx, ok := lc.Search(func(item int, _ int) bool { return item == 20 }) + + if !ok || idx != 1 { + t.Errorf("expected index 1, got %d", idx) + } +} + +func TestBefore(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + v, ok := lc.Before(func(item int, _ int) bool { return item == 3 }) + + if !ok || v != 2 { + t.Errorf("expected 2, got %d", v) + } +} + +func TestAfter(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + v, ok := lc.After(func(item int, _ int) bool { return item == 3 }) + + if !ok || v != 4 { + t.Errorf("expected 4, got %d", v) + } +} + +func TestFilter(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + filtered := lc.Filter(func(item int, _ int) bool { return item%2 == 0 }) + expected := []int{2, 4} + + if !reflect.DeepEqual(filtered.All(), expected) { + t.Errorf("expected %v, got %v", expected, filtered.All()) + } +} + +func TestReject(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + rejected := lc.Reject(func(item int, _ int) bool { return item%2 == 0 }) + expected := []int{1, 3, 5} + + if !reflect.DeepEqual(rejected.All(), expected) { + t.Errorf("expected %v, got %v", expected, rejected.All()) + } +} + +func TestMap(t *testing.T) { + lc := From([]int{1, 2, 3}) + mapped := Map(lc, func(item int, _ int) int { return item * 2 }) + expected := []int{2, 4, 6} + + if !reflect.DeepEqual(mapped.All(), expected) { + t.Errorf("expected %v, got %v", expected, mapped.All()) + } +} + +func TestFlatMap(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := FlatMap(lc, func(item int, _ int) []int { return []int{item, item * 10} }) + expected := []int{1, 10, 2, 20, 3, 30} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestTake(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + taken := lc.Take(3) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(taken.All(), expected) { + t.Errorf("expected %v, got %v", expected, taken.All()) + } + + // Negative take + taken2 := lc.Take(-2) + expected2 := []int{4, 5} + + if !reflect.DeepEqual(taken2.All(), expected2) { + t.Errorf("expected %v, got %v", expected2, taken2.All()) + } +} + +func TestTakeUntil(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + result := lc.TakeUntil(func(item int, _ int) bool { return item == 4 }) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestTakeWhile(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + result := lc.TakeWhile(func(item int, _ int) bool { return item < 4 }) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestSkip(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + result := lc.Skip(2) + expected := []int{3, 4, 5} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestSkipUntil(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + result := lc.SkipUntil(func(item int, _ int) bool { return item == 3 }) + expected := []int{3, 4, 5} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestSkipWhile(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + result := lc.SkipWhile(func(item int, _ int) bool { return item < 3 }) + expected := []int{3, 4, 5} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestSlice(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + result := lc.Slice(1, 3) + expected := []int{2, 3, 4} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestChunk(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + chunks := lc.Chunk(2) + + if len(chunks) != 3 { + t.Errorf("expected 3 chunks, got %d", len(chunks)) + } + + if !reflect.DeepEqual(chunks[0], []int{1, 2}) { + t.Errorf("expected [1 2], got %v", chunks[0]) + } + + if !reflect.DeepEqual(chunks[2], []int{5}) { + t.Errorf("expected [5], got %v", chunks[2]) + } +} + +func TestNth(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5, 6, 7, 8}) + result := lc.Nth(3) + expected := []int{1, 4, 7} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestConcat(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.Concat([]int{4, 5}) + expected := []int{1, 2, 3, 4, 5} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestPad(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.Pad(5, 0) + expected := []int{1, 2, 3, 0, 0} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestEvery(t *testing.T) { + lc := From([]int{2, 4, 6, 8}) + + if !lc.Every(func(item int, _ int) bool { return item%2 == 0 }) { + t.Error("expected all even") + } +} + +func TestImplode(t *testing.T) { + lc := From([]string{"a", "b", "c"}) + result := lc.Implode(", ") + + if result != "a, b, c" { + t.Errorf("expected 'a, b, c', got '%s'", result) + } +} + +func TestJoin(t *testing.T) { + lc := From([]string{"a", "b", "c"}) + result := lc.Join(", ", " and ") + + if result != "a, b and c" { + t.Errorf("expected 'a, b and c', got '%s'", result) + } +} + +func TestReduce(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + sum := Reduce(lc, func(carry int, item int, _ int) int { return carry + item }, 0) + + if sum != 15 { + t.Errorf("expected 15, got %d", sum) + } +} + +func TestUnique(t *testing.T) { + lc := From([]int{1, 2, 2, 3, 3, 3}) + result := Unique(lc, func(item int) int { return item }) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestPluck(t *testing.T) { + type User struct { + Name string + Age int + } + + lc := From([]User{{"Alice", 25}, {"Bob", 30}}) + names := Pluck(lc, func(u User) string { return u.Name }) + expected := []string{"Alice", "Bob"} + + if !reflect.DeepEqual(names.All(), expected) { + t.Errorf("expected %v, got %v", expected, names.All()) + } +} + +func TestGroupBy(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5, 6}) + groups := GroupBy(lc, func(item int) string { + if item%2 == 0 { + return "even" + } + + return "odd" + }) + + if len(groups["even"].All()) != 3 { + t.Errorf("expected 3 evens, got %d", len(groups["even"].All())) + } +} + +func TestKeyBy(t *testing.T) { + type User struct { + ID int + Name string + } + + lc := From([]User{{1, "Alice"}, {2, "Bob"}}) + keyed := KeyBy(lc, func(u User) int { return u.ID }) + + if keyed[1].Name != "Alice" { + t.Error("expected Alice") + } +} + +func TestCountBy(t *testing.T) { + lc := From([]string{"apple", "banana", "apple"}) + counts := CountBy(lc, func(item string) string { return item }) + + if counts["apple"] != 2 { + t.Errorf("expected 2, got %d", counts["apple"]) + } +} + +func TestRemember(t *testing.T) { + callCount := 0 + lc := New(func(yield func(int) bool) { + callCount++ + + for i := 1; i <= 3; i++ { + if !yield(i) { + return + } + } + }) + + remembered := lc.Remember() + // First evaluation + remembered.All() + // Second evaluation - should use cache + remembered.All() + + if callCount != 1 { + t.Errorf("expected source called once, got %d", callCount) + } +} + +func TestContainsOneItem(t *testing.T) { + if !From([]int{1}).ContainsOneItem() { + t.Error("expected true") + } + + if From([]int{1, 2}).ContainsOneItem() { + t.Error("expected false") + } +} + +func TestWhen(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.When(true, func(lc *Collection[int]) *Collection[int] { + return lc.Take(2) + }) + + if result.Count() != 2 { + t.Errorf("expected 2, got %d", result.Count()) + } +} + +func TestTapEach(t *testing.T) { + lc := From([]int{1, 2, 3}) + sum := 0 + result := lc.TapEach(func(item int, _ int) { + sum += item + }) + // TapEach is lazy, need to evaluate + result.All() + + if sum != 6 { + t.Errorf("expected 6, got %d", sum) + } +} + +func TestFromSlice(t *testing.T) { + items := []int{1, 2, 3} + lc := From(items) + + if lc.Count() != 3 { + t.Errorf("expected 3, got %d", lc.Count()) + } +} + +func TestEach(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + sum := 0 + lc.Each(func(item int, _ int) bool { + sum += item + + return item < 3 + }) + + if sum != 6 { // 1+2+3 + t.Errorf("expected 6, got %d", sum) + } +} + +func TestSole(t *testing.T) { + lc := From([]int{1, 2, 3}) + v, err := lc.Sole(func(item int, _ int) bool { return item == 2 }) + + if err != nil || v != 2 { + t.Errorf("expected 2, got %d, err: %v", v, err) + } +} + +func TestFirstOrFail(t *testing.T) { + lc := From([]int{1, 2, 3}) + v, err := lc.FirstOrFail() + + if err != nil || v != 1 { + t.Errorf("expected 1, got %d, err: %v", v, err) + } + + empty := Empty[int]() + _, err = empty.FirstOrFail() + + if err == nil { + t.Error("expected error") + } +} + +func TestIter(t *testing.T) { + lc := From([]int{10, 20, 30}) + sum := 0 + + for item := range lc.Iter() { + sum += item + } + + if sum != 60 { + t.Errorf("expected 60, got %d", sum) + } +} + +func TestCollect(t *testing.T) { + lc := From([]int{1, 2, 3}) + items := lc.Collect() + + if !reflect.DeepEqual(items, []int{1, 2, 3}) { + t.Errorf("expected [1 2 3], got %v", items) + } +} + +func TestContainsManyItems(t *testing.T) { + if !From([]int{1, 2}).ContainsManyItems() { + t.Error("expected true for 2 items") + } + + if From([]int{1}).ContainsManyItems() { + t.Error("expected false for 1 item") + } + + if Empty[int]().ContainsManyItems() { + t.Error("expected false for empty") + } +} + +func TestSome(t *testing.T) { + lc := From([]int{1, 2, 3}) + + if !lc.Some(func(item int, _ int) bool { return item == 2 }) { + t.Error("expected true") + } + + if lc.Some(func(item int, _ int) bool { return item > 10 }) { + t.Error("expected false") + } +} + +func TestDoesntContain(t *testing.T) { + lc := From([]int{1, 2, 3}) + + if !lc.DoesntContain(func(item int, _ int) bool { return item == 99 }) { + t.Error("expected true") + } + + if lc.DoesntContain(func(item int, _ int) bool { return item == 2 }) { + t.Error("expected false") + } +} + +func TestTap(t *testing.T) { + lc := From([]int{1, 2, 3}) + called := false + result := lc.Tap(func(c *Collection[int]) { + called = true + }) + + if !called { + t.Error("expected callback to be called") + } + + if result != lc { + t.Error("expected Tap to return self") + } +} + +func TestHas(t *testing.T) { + lc := From([]int{10, 20, 30}) + + if !lc.Has(0) { + t.Error("expected true for index 0") + } + + if !lc.Has(2) { + t.Error("expected true for index 2") + } + + if lc.Has(5) { + t.Error("expected false for index 5") + } +} + +func TestHasAny(t *testing.T) { + lc := From([]int{10, 20, 30}) + + if !lc.HasAny(0, 5) { + t.Error("expected true when at least one valid") + } + + if lc.HasAny(5, 6, 7) { + t.Error("expected false when all invalid") + } +} + +func TestHasSole(t *testing.T) { + single := From([]int{42}) + + if !single.HasSole() { + t.Error("expected true for single item without predicate") + } + + multi := From([]int{1, 2, 3}) + + if multi.HasSole() { + t.Error("expected false for multiple items without predicate") + } + + if !multi.HasSole(func(item int, _ int) bool { return item == 2 }) { + t.Error("expected true for single match") + } + + if multi.HasSole(func(item int, _ int) bool { return item > 1 }) { + t.Error("expected false for multiple matches") + } + + if multi.HasSole(func(item int, _ int) bool { return item > 10 }) { + t.Error("expected false for no matches") + } +} + +func TestChunkWhile(t *testing.T) { + lc := From([]int{1, 1, 2, 2, 3}) + chunks := lc.ChunkWhile(func(item int, _ int, current []int) bool { + return item == current[len(current)-1] + }) + + if len(chunks) != 3 { + t.Errorf("expected 3 chunks, got %d", len(chunks)) + } + + if !reflect.DeepEqual(chunks[0], []int{1, 1}) { + t.Errorf("expected [1 1], got %v", chunks[0]) + } + + if !reflect.DeepEqual(chunks[1], []int{2, 2}) { + t.Errorf("expected [2 2], got %v", chunks[1]) + } + + if !reflect.DeepEqual(chunks[2], []int{3}) { + t.Errorf("expected [3], got %v", chunks[2]) + } +} + +func TestTakeUntilTimeout(t *testing.T) { + lc := From([]int{1, 2, 3, 4, 5}) + result := lc.TakeUntilTimeout(time.Second) + items := result.All() + + if !reflect.DeepEqual(items, []int{1, 2, 3, 4, 5}) { + t.Errorf("expected all items with generous timeout, got %v", items) + } +} + +func TestThrottle(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.Throttle(time.Millisecond) + items := result.All() + + if !reflect.DeepEqual(items, []int{1, 2, 3}) { + t.Errorf("expected [1 2 3], got %v", items) + } +} + +func TestWhenFalseWithDefault(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.When(false, func(c *Collection[int]) *Collection[int] { + return c.Take(1) + }, func(c *Collection[int]) *Collection[int] { + return c.Take(2) + }) + + if result.Count() != 2 { + t.Errorf("expected 2 (default applied), got %d", result.Count()) + } +} + +func TestWhenFalseWithoutDefault(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.When(false, func(c *Collection[int]) *Collection[int] { + return c.Take(1) + }) + + if result.Count() != 3 { + t.Errorf("expected 3 (unchanged), got %d", result.Count()) + } +} + +func TestWhenEmpty(t *testing.T) { + empty := Empty[int]() + result := empty.WhenEmpty(func(c *Collection[int]) *Collection[int] { + return From([]int{1, 2, 3}) + }) + + if result.Count() != 3 { + t.Errorf("expected 3, got %d", result.Count()) + } + + nonEmpty := From([]int{1}) + called := false + nonEmpty.WhenEmpty(func(c *Collection[int]) *Collection[int] { + called = true + + return c + }) + + if called { + t.Error("expected callback not called for non-empty") + } +} + +func TestWhenNotEmpty(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.WhenNotEmpty(func(c *Collection[int]) *Collection[int] { + return c.Take(1) + }) + + if result.Count() != 1 { + t.Errorf("expected 1, got %d", result.Count()) + } + + empty := Empty[int]() + called := false + empty.WhenNotEmpty(func(c *Collection[int]) *Collection[int] { + called = true + + return c + }) + + if called { + t.Error("expected callback not called for empty") + } +} + +func TestUnless(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.Unless(false, func(c *Collection[int]) *Collection[int] { + return c.Take(2) + }) + + if result.Count() != 2 { + t.Errorf("expected 2, got %d", result.Count()) + } + + result2 := lc.Unless(true, func(c *Collection[int]) *Collection[int] { + return c.Take(1) + }) + + if result2.Count() != 3 { + t.Errorf("expected 3 (unchanged), got %d", result2.Count()) + } +} + +func TestDump(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.Dump() + items := result.All() + + if !reflect.DeepEqual(items, []int{1, 2, 3}) { + t.Errorf("expected [1 2 3], got %v", items) + } +} + +func TestSoleEmpty(t *testing.T) { + lc := Empty[int]() + _, err := lc.Sole() + + if err == nil { + t.Error("expected error for empty collection") + } +} + +func TestSoleMultiple(t *testing.T) { + lc := From([]int{1, 2, 3}) + _, err := lc.Sole() + + if err == nil { + t.Error("expected error for multiple items") + } +} + +func TestSoleSingle(t *testing.T) { + lc := From([]int{42}) + v, err := lc.Sole() + + if err != nil || v != 42 { + t.Errorf("expected 42, got %d, err: %v", v, err) + } +} + +func TestSoleCallbackNoMatch(t *testing.T) { + lc := From([]int{1, 2, 3}) + _, err := lc.Sole(func(item int, _ int) bool { return item > 10 }) + + if err == nil { + t.Error("expected error when no match") + } +} + +func TestSoleCallbackMultipleMatches(t *testing.T) { + lc := From([]int{1, 2, 3}) + _, err := lc.Sole(func(item int, _ int) bool { return item > 1 }) + + if err == nil { + t.Error("expected error for multiple matches") + } +} + +func TestPadNegative(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.Pad(-5, 0) + expected := []int{0, 0, 1, 2, 3} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v, got %v", expected, result.All()) + } +} + +func TestPadAlreadySufficient(t *testing.T) { + lc := From([]int{1, 2, 3}) + result := lc.Pad(2, 0) + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(result.All(), expected) { + t.Errorf("expected %v (unchanged), got %v", expected, result.All()) + } +} + +func TestRangeSingle(t *testing.T) { + lc := Range(5, 5) + expected := []int{5} + + if !reflect.DeepEqual(lc.All(), expected) { + t.Errorf("expected %v, got %v", expected, lc.All()) + } +} + +func TestTimesZero(t *testing.T) { + lc := Times(0, func(i int) int { return i }) + + if lc.Count() != 0 { + t.Errorf("expected 0, got %d", lc.Count()) + } +} + +func TestIsEmptyNonEmpty(t *testing.T) { + lc := From([]int{1}) + + if lc.IsEmpty() { + t.Error("expected false for non-empty") + } +} + +func TestEveryEmpty(t *testing.T) { + lc := Empty[int]() + + if !lc.Every(func(item int, _ int) bool { return false }) { + t.Error("expected true for empty collection") + } +} + +func TestEveryFailing(t *testing.T) { + lc := From([]int{1, 2, 3}) + + if lc.Every(func(item int, _ int) bool { return item > 2 }) { + t.Error("expected false") + } +} diff --git a/lazy/combine.go b/lazy/combine.go new file mode 100644 index 0000000..ec22ea2 --- /dev/null +++ b/lazy/combine.go @@ -0,0 +1,45 @@ +package lazy + +// GroupBy groups items by the key returned by the given function. +// This requires eager evaluation to build the groups. +func GroupBy[T any, K comparable](lc *Collection[T], keyFunc func(T) K) map[K]*Collection[T] { + groups := make(map[K][]T) + lc.source(func(item T) bool { + key := keyFunc(item) + groups[key] = append(groups[key], item) + + return true + }) + result := make(map[K]*Collection[T]) + + for key, items := range groups { + result[key] = From(items) + } + + return result +} + +// KeyBy indexes items by the key returned by the given function. +// Duplicate keys cause the later value to overwrite the earlier one. +func KeyBy[T any, K comparable](lc *Collection[T], keyFunc func(T) K) map[K]T { + result := make(map[K]T) + lc.source(func(item T) bool { + result[keyFunc(item)] = item + + return true + }) + + return result +} + +// CountBy counts occurrences of each key returned by the given function. +func CountBy[T any, K comparable](lc *Collection[T], keyFunc func(T) K) map[K]int { + result := make(map[K]int) + lc.source(func(item T) bool { + result[keyFunc(item)]++ + + return true + }) + + return result +} diff --git a/lazy/doc.go b/lazy/doc.go new file mode 100644 index 0000000..bf7caa5 --- /dev/null +++ b/lazy/doc.go @@ -0,0 +1,15 @@ +// Package lazy provides lazily evaluated generic sequences backed by [iter.Seq]. +// +// [Collection] it wraps an iterator and provides a fluent API for filtering, +// transforming, and consuming data on demand. Nothing is computed until the +// collection is materialised, making it ideal for large datasets, pipelines +// with early termination, and infinite sequences. +// +// To create a lazy collection from a slice: +// +// lc := lazy.From([]int{1, 2, 3}) +// +// To convert back to a slice: +// +// items := lc.Eager() +package lazy diff --git a/lazy/partition.go b/lazy/partition.go new file mode 100644 index 0000000..c277dd1 --- /dev/null +++ b/lazy/partition.go @@ -0,0 +1,258 @@ +package lazy + +import "time" + +// Take returns a new lazy collection with at most limit items. +// A negative limit takes from the end (requires eager evaluation). +func (lc *Collection[T]) Take(limit int) *Collection[T] { + if limit < 0 { + // For negative take, we need to eagerly evaluate + items := lc.All() + start := len(items) + limit + + if start < 0 { + start = 0 + } + + return From(items[start:]) + } + + return New(func(yield func(T) bool) { + count := 0 + lc.source(func(item T) bool { + if count >= limit { + return false + } + + count++ + + return yield(item) + }) + }) +} + +// TakeUntil returns items from the beginning until the callback returns true. +func (lc *Collection[T]) TakeUntil(callback func(T, int) bool) *Collection[T] { + return New(func(yield func(T) bool) { + idx := 0 + lc.source(func(item T) bool { + if callback(item, idx) { + return false + } + + idx++ + + return yield(item) + }) + }) +} + +// TakeWhile returns items from the beginning while the callback returns true. +func (lc *Collection[T]) TakeWhile(callback func(T, int) bool) *Collection[T] { + return lc.TakeUntil(func(item T, idx int) bool { + return !callback(item, idx) + }) +} + +// TakeUntilTimeout returns items until the given duration has elapsed. +func (lc *Collection[T]) TakeUntilTimeout(timeout time.Duration) *Collection[T] { + return New(func(yield func(T) bool) { + deadline := time.Now().Add(timeout) + lc.source(func(item T) bool { + if time.Now().After(deadline) { + return false + } + + return yield(item) + }) + }) +} + +// Skip returns a new lazy collection that skips the first count items. +func (lc *Collection[T]) Skip(count int) *Collection[T] { + return New(func(yield func(T) bool) { + skipped := 0 + lc.source(func(item T) bool { + if skipped < count { + skipped++ + + return true + } + + return yield(item) + }) + }) +} + +// SkipUntil skips items until the callback returns true, then yields the rest. +func (lc *Collection[T]) SkipUntil(callback func(T, int) bool) *Collection[T] { + return New(func(yield func(T) bool) { + found := false + idx := 0 + lc.source(func(item T) bool { + if !found && callback(item, idx) { + found = true + } + + if found { + return yield(item) + } + + idx++ + + return true + }) + }) +} + +// SkipWhile skips items while the callback returns true, then yields the rest. +func (lc *Collection[T]) SkipWhile(callback func(T, int) bool) *Collection[T] { + return lc.SkipUntil(func(item T, idx int) bool { + return !callback(item, idx) + }) +} + +// Slice returns a subset of the lazy collection starting at offset with an optional length. +func (lc *Collection[T]) Slice(offset int, lengths ...int) *Collection[T] { + result := lc.Skip(offset) + + if len(lengths) > 0 { + result = result.Take(lengths[0]) + } + + return result +} + +// Chunk eagerly evaluates the lazy collection and splits items into groups of the given size. +func (lc *Collection[T]) Chunk(size int) [][]T { + var result [][]T + chunk := make([]T, 0, size) + lc.source(func(item T) bool { + chunk = append(chunk, item) + + if len(chunk) >= size { + result = append(result, chunk) + chunk = make([]T, 0, size) + } + + return true + }) + + if len(chunk) > 0 { + result = append(result, chunk) + } + + return result +} + +// ChunkWhile splits the lazy collection into groups where consecutive items satisfy the callback. +// A new chunk is started when the callback returns false. +func (lc *Collection[T]) ChunkWhile(callback func(T, int, []T) bool) [][]T { + var result [][]T + current := make([]T, 0) + idx := 0 + lc.source(func(item T) bool { + if len(current) == 0 || callback(item, idx, current) { + current = append(current, item) + } else { + result = append(result, current) + current = []T{item} + } + + idx++ + + return true + }) + + if len(current) > 0 { + result = append(result, current) + } + + return result +} + +// Nth returns every step-th element, starting from an optional offset. +func (lc *Collection[T]) Nth(step int, offsets ...int) *Collection[T] { + offset := 0 + + if len(offsets) > 0 { + offset = offsets[0] + } + + return New(func(yield func(T) bool) { + idx := 0 + lc.source(func(item T) bool { + if idx >= offset && (idx-offset)%step == 0 { + if !yield(item) { + return false + } + } + + idx++ + + return true + }) + }) +} + +// Concat returns a new lazy collection with the given items appended. +func (lc *Collection[T]) Concat(items []T) *Collection[T] { + return New(func(yield func(T) bool) { + lc.source(func(item T) bool { + return yield(item) + }) + + for _, item := range items { + if !yield(item) { + return + } + } + }) +} + +// Pad returns a new lazy collection padded to the given size with the specified value. +// A negative size pads at the beginning; a positive size pads at the end. +func (lc *Collection[T]) Pad(size int, value T) *Collection[T] { + return New(func(yield func(T) bool) { + count := 0 + absSize := size + + if absSize < 0 { + absSize = -absSize + } + + if size < 0 { + // Evaluate to know count + items := lc.All() + padCount := absSize - len(items) + + if padCount > 0 { + for i := 0; i < padCount; i++ { + if !yield(value) { + return + } + } + } + + for _, item := range items { + if !yield(item) { + return + } + } + } else { + lc.source(func(item T) bool { + count++ + + return yield(item) + }) + + for count < size { + if !yield(value) { + return + } + + count++ + } + } + }) +} diff --git a/lazy/pipeline.go b/lazy/pipeline.go new file mode 100644 index 0000000..5d580e2 --- /dev/null +++ b/lazy/pipeline.go @@ -0,0 +1,148 @@ +package lazy + +import ( + "fmt" + "strings" + "time" + + "github.com/gocanto/collection/arr" +) + +// Each iterates over items, calling the callback for each one. +// If the callback returns false, iteration stops early. +func (lc *Collection[T]) Each(callback func(T, int) bool) *Collection[T] { + idx := 0 + lc.source(func(item T) bool { + cont := callback(item, idx) + idx++ + + return cont + }) + + return lc +} + +// Tap passes the lazy collection to the given callback and returns it unchanged. +func (lc *Collection[T]) Tap(callback func(*Collection[T])) *Collection[T] { + callback(lc) + + return lc +} + +// TapEach returns a new lazy collection that calls the callback on each item as it passes through. +func (lc *Collection[T]) TapEach(callback func(T, int)) *Collection[T] { + return New(func(yield func(T) bool) { + idx := 0 + lc.source(func(item T) bool { + callback(item, idx) + idx++ + + return yield(item) + }) + }) +} + +// Throttle returns a new lazy collection that inserts a delay between each yielded item. +func (lc *Collection[T]) Throttle(delay time.Duration) *Collection[T] { + return New(func(yield func(T) bool) { + first := true + lc.source(func(item T) bool { + if !first { + time.Sleep(delay) + } + + first = false + + return yield(item) + }) + }) +} + +// Remember returns a lazy collection that caches items on the first iteration, +// so later iterations reuse the cached values. +func (lc *Collection[T]) Remember() *Collection[T] { + var cache []T + cached := false + + return New(func(yield func(T) bool) { + if cached { + for _, item := range cache { + if !yield(item) { + return + } + } + + return + } + + cache = make([]T, 0) + lc.source(func(item T) bool { + cache = append(cache, item) + + return yield(item) + }) + cached = true + }) +} + +// Implode joins all items into a single string separated by the given glue. +func (lc *Collection[T]) Implode(glue string) string { + parts := make([]string, 0) + lc.source(func(item T) bool { + parts = append(parts, fmt.Sprint(item)) + + return true + }) + + return strings.Join(parts, glue) +} + +// Join joins all items into a string with the given separator. +// An optional final separator can be provided for the last element. +func (lc *Collection[T]) Join(glue string, finalGlues ...string) string { + parts := make([]string, 0) + lc.source(func(item T) bool { + parts = append(parts, fmt.Sprint(item)) + + return true + }) + + return arr.Join(parts, glue, finalGlues...) +} + +// When applies the callback if the condition is true; otherwise applies the optional default. +func (lc *Collection[T]) When(condition bool, callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + if condition { + return callback(lc) + } + + if len(defaults) > 0 { + return defaults[0](lc) + } + + return lc +} + +// WhenEmpty applies the callback if the lazy collection is empty. +func (lc *Collection[T]) WhenEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + return lc.When(lc.IsEmpty(), callback, defaults...) +} + +// WhenNotEmpty applies the callback if the lazy collection is not empty. +func (lc *Collection[T]) WhenNotEmpty(callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + return lc.When(lc.IsNotEmpty(), callback, defaults...) +} + +// Unless applies the callback unless the condition is true. +func (lc *Collection[T]) Unless(condition bool, callback func(*Collection[T]) *Collection[T], defaults ...func(*Collection[T]) *Collection[T]) *Collection[T] { + return lc.When(!condition, callback, defaults...) +} + +// Dump prints the lazy collection items to stdout for debugging and returns a new +// lazy collection backed by the evaluated items. +func (lc *Collection[T]) Dump() *Collection[T] { + items := lc.All() + fmt.Printf("%v\n", items) + + return From(items) +} diff --git a/lazy/transform.go b/lazy/transform.go new file mode 100644 index 0000000..30e3a7a --- /dev/null +++ b/lazy/transform.go @@ -0,0 +1,104 @@ +package lazy + +// Filter returns a new lazy collection containing only items for which the callback returns true. +func (lc *Collection[T]) Filter(callback func(T, int) bool) *Collection[T] { + return New(func(yield func(T) bool) { + idx := 0 + lc.source(func(item T) bool { + if callback(item, idx) { + if !yield(item) { + return false + } + } + + idx++ + + return true + }) + }) +} + +// Reject returns a new lazy collection excluding items for which the callback returns true. +func (lc *Collection[T]) Reject(callback func(T, int) bool) *Collection[T] { + return lc.Filter(func(item T, index int) bool { + return !callback(item, index) + }) +} + +// Map transforms each item using the callback, returning a new lazy collection. +func Map[T any, R any](lc *Collection[T], callback func(T, int) R) *Collection[R] { + return New(func(yield func(R) bool) { + idx := 0 + lc.source(func(item T) bool { + if !yield(callback(item, idx)) { + return false + } + + idx++ + + return true + }) + }) +} + +// FlatMap transforms each item into a slice and flattens the results into a new lazy collection. +func FlatMap[T any, R any](lc *Collection[T], callback func(T, int) []R) *Collection[R] { + return New(func(yield func(R) bool) { + idx := 0 + lc.source(func(item T) bool { + for _, r := range callback(item, idx) { + if !yield(r) { + return false + } + } + + idx++ + + return true + }) + }) +} + +// Every report whether all items satisfy the callback. +func (lc *Collection[T]) Every(callback func(T, int) bool) bool { + result := true + idx := 0 + lc.source(func(item T) bool { + if !callback(item, idx) { + result = false + + return false + } + + idx++ + + return true + }) + + return result +} + +// Unique returns a lazy collection containing only unique items as determined by the key function. +func Unique[T any, K comparable](lc *Collection[T], keyFunc func(T) K) *Collection[T] { + return New(func(yield func(T) bool) { + seen := make(map[K]bool) + lc.source(func(item T) bool { + key := keyFunc(item) + + if !seen[key] { + seen[key] = true + + return yield(item) + } + + return true + }) + }) +} + +// Pluck extracts a value from each item using the given function, returning a new lazy collection. +func Pluck[T any, V any](lc *Collection[T], valueFunc func(T) V) *Collection[V] { + return Map(lc, func(item T, _ int) V { + return valueFunc(item) + }) +} diff --git a/support/doc.go b/support/doc.go new file mode 100644 index 0000000..e2df5a0 --- /dev/null +++ b/support/doc.go @@ -0,0 +1,10 @@ +// Package support provides shared types and error definitions used across +// the collection family of packages. +// +// Exported types: +// +// - [Pair] β€” a generic key-value pair. +// - [Numeric] β€” a type constraint covering all built-in numeric types. +// - [ItemNotFoundError] β€” returned when a lookup finds no matching item. +// - [MultipleItemsFoundError] β€” returned when a single item is expected but several matches. +package support diff --git a/collection/errors.go b/support/errors.go similarity index 91% rename from collection/errors.go rename to support/errors.go index 4ed4707..d277fb7 100644 --- a/collection/errors.go +++ b/support/errors.go @@ -1,4 +1,4 @@ -package collection +package support import "fmt" @@ -7,22 +7,24 @@ type ItemNotFoundError struct { Message string } +// MultipleItemsFoundError is returned when a single item is expected, but multiple is found. +type MultipleItemsFoundError struct { + Count int + Message string +} + func (e *ItemNotFoundError) Error() string { if e.Message != "" { return e.Message } - return "item not found" -} -// MultipleItemsFoundError is returned when a single item is expected but multiple are found. -type MultipleItemsFoundError struct { - Count int - Message string + return "item not found" } func (e *MultipleItemsFoundError) Error() string { if e.Message != "" { return e.Message } + return fmt.Sprintf("multiple items found: %d items", e.Count) -} \ No newline at end of file +} diff --git a/support/errors_test.go b/support/errors_test.go new file mode 100644 index 0000000..525455d --- /dev/null +++ b/support/errors_test.go @@ -0,0 +1,37 @@ +package support + +import ( + "testing" +) + +func TestItemNotFoundErrorDefault(t *testing.T) { + err := &ItemNotFoundError{} + + if err.Error() != "item not found" { + t.Errorf("expected 'item not found', got '%s'", err.Error()) + } +} + +func TestItemNotFoundErrorCustom(t *testing.T) { + err := &ItemNotFoundError{Message: "user not found"} + + if err.Error() != "user not found" { + t.Errorf("expected 'user not found', got '%s'", err.Error()) + } +} + +func TestMultipleItemsFoundErrorDefault(t *testing.T) { + err := &MultipleItemsFoundError{Count: 5} + + if err.Error() != "multiple items found: 5 items" { + t.Errorf("expected 'multiple items found: 5 items', got '%s'", err.Error()) + } +} + +func TestMultipleItemsFoundErrorCustom(t *testing.T) { + err := &MultipleItemsFoundError{Count: 3, Message: "too many users"} + + if err.Error() != "too many users" { + t.Errorf("expected 'too many users', got '%s'", err.Error()) + } +} diff --git a/collection/types.go b/support/types.go similarity index 93% rename from collection/types.go rename to support/types.go index 9cedba5..7ac64a2 100644 --- a/collection/types.go +++ b/support/types.go @@ -1,4 +1,4 @@ -package collection +package support // Numeric is a constraint for numeric types. type Numeric interface {