mirror of
https://github.com/rjNemo/underscore
synced 2026-06-06 10:36:43 +00:00
Compare commits
24 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a29f64b700 | |||
| f33e86d502 | |||
| c8b01aacc2 | |||
| 85f73f63a9 | |||
| 4f02db2da7 | |||
| 260c48e051 | |||
| 5240c27fcd | |||
| 0bf04c224e | |||
| b35a87e50c | |||
| 3617c2de8f | |||
| bcb4dd1e9d | |||
| 2651a3331a | |||
| d622c8cba8 | |||
| 6576c4fea7 | |||
| 07d05425bb | |||
| 75eddcdde5 | |||
| b04e545d03 | |||
| 40ac16261e | |||
| 106b713cc5 | |||
| a1943556b4 | |||
| 46d52e3cfa | |||
| 7caa23e082 | |||
| 92b64630dc | |||
| 7580836815 |
71 changed files with 2583 additions and 51 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -60,3 +60,7 @@ Temporary Items
|
||||||
docs/public
|
docs/public
|
||||||
.trivycache/
|
.trivycache/
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
|
.claude
|
||||||
|
AGENTS.md
|
||||||
|
bench*txt
|
||||||
|
ACTION_PLAN.md
|
||||||
|
|
|
||||||
183
CLAUDE.md
Normal file
183
CLAUDE.md
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
`underscore` is a Go library providing functional programming helpers inspired by underscore.js, built on Go 1.18+ generics. The library is organized as a flat structure with individual files for each function, plus a `maps` subpackage for map-specific utilities.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Run all tests (local)
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run all tests with coverage (local)
|
||||||
|
go test ./... -coverpkg=./... -coverprofile cov.out -covermode=count
|
||||||
|
go tool cover -func cov.out
|
||||||
|
rm cov.out
|
||||||
|
|
||||||
|
# Run tests in Docker (preferred for CI/validation)
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
go test -run TestFunctionName
|
||||||
|
|
||||||
|
# Run tests for a specific file
|
||||||
|
go test -run TestMap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build Docker image
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting & Security
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Scan Docker image for vulnerabilities
|
||||||
|
make scan
|
||||||
|
|
||||||
|
# Scan config files
|
||||||
|
make scan-config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Serve docs locally at http://localhost:1313
|
||||||
|
make docs
|
||||||
|
|
||||||
|
# Build static docs
|
||||||
|
make build-docs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Code Organization
|
||||||
|
|
||||||
|
The library uses a **flat structure** where each function is implemented in its own file:
|
||||||
|
|
||||||
|
- `<function>.go` - implementation
|
||||||
|
- `<function>_test.go` - tests
|
||||||
|
|
||||||
|
Example: `filter.go` + `filter_test.go`, `map.go` + `map_test.go`
|
||||||
|
|
||||||
|
### Core Patterns
|
||||||
|
|
||||||
|
**Generic Functions**: Most functions use Go generics with constraints from `cmp.Ordered` or custom type parameters. Functions operate on slices and return new slices (immutable style).
|
||||||
|
|
||||||
|
**Pipe Chain**: The `Pipe[T]` struct enables method chaining for ordered types. Methods that return slices continue the chain, while methods that return values (like `All`, `Any`, `Reduce`) break the chain and return the final value.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// pipe.go defines Pipe[T cmp.Ordered]
|
||||||
|
// Chain-continuing: Filter, Map
|
||||||
|
// Chain-breaking: All, Any, Reduce, Min, Max, Partition, Find, Each
|
||||||
|
```
|
||||||
|
|
||||||
|
**Concurrency Helpers**: `ParallelMap` and `ParallelFilter` use worker pools with:
|
||||||
|
|
||||||
|
- Context-based cancellation
|
||||||
|
- Order preservation (results match input order)
|
||||||
|
- First-error-wins semantics
|
||||||
|
- Default workers = GOMAXPROCS if workers <= 0
|
||||||
|
|
||||||
|
Implementation detail: Uses `sync.Once` to capture first error and cancel context immediately.
|
||||||
|
|
||||||
|
**Subpackages**:
|
||||||
|
|
||||||
|
- `maps/` - Map-specific utilities (`Keys`, `Values`, `Map`)
|
||||||
|
- Uses type alias `M[K, V] = map[K]V` for cleaner signatures
|
||||||
|
- `Map` function allows transforming map entries
|
||||||
|
|
||||||
|
### Testing Conventions
|
||||||
|
|
||||||
|
- Use `testify/assert` for assertions
|
||||||
|
- Test file names match source files with `_test.go` suffix
|
||||||
|
- Table-driven tests are common (see `map_test.go`, `filter_test.go`)
|
||||||
|
- Internal tests (using `package underscore` rather than `package underscore_test`) are used sparingly for testing unexported functions
|
||||||
|
|
||||||
|
## Key Constraints
|
||||||
|
|
||||||
|
- **Minimum Go version**: 1.24.2 (see go.mod)
|
||||||
|
- **Generic constraints**: Most collection functions require `cmp.Ordered` types; some use `comparable` or no constraints
|
||||||
|
- **Order preservation**: `ParallelMap` and `ParallelFilter` guarantee output order matches input order
|
||||||
|
- **No mutation**: Functions return new slices; `UniqueInPlace` is the exception (in-place deduplication)
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
### Recently Fixed (2025-11-14)
|
||||||
|
|
||||||
|
1. ✅ **Filter allocation** - Now pre-allocates with `make([]T, 0, len(values))` (90% fewer allocations)
|
||||||
|
2. ✅ **OrderBy algorithm** - Replaced bubble sort with `slices.SortFunc` (629x faster for large datasets)
|
||||||
|
3. ✅ **Partition allocation** - Now pre-allocates both result slices
|
||||||
|
4. ✅ **Max/Min empty slices** - Now panics with clear message: "underscore.Max: empty slice"
|
||||||
|
5. ✅ **Drop semantics** - Fixed to drop first N elements (breaking change). Old behavior available as `RemoveAt`
|
||||||
|
|
||||||
|
### API Design Issues
|
||||||
|
|
||||||
|
1. **Pipe constraint** (`pipe.go:7`) - `Pipe[T cmp.Ordered]` prevents usage with custom types
|
||||||
|
2. **Last panics** (`last.go:5-8`) - No empty slice handling
|
||||||
|
|
||||||
|
### Missing Features
|
||||||
|
|
||||||
|
Popular FP utilities not yet implemented: `TakeWhile`, `DropWhile`, `Scan`, `First/FirstN`, `Init`, `Intersperse`, `Sliding`, `FoldRight`, `Tap`, `Transpose`, `Unzip`, `ParallelReduce`, `Replicate`
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
### Good Performance Patterns
|
||||||
|
- `Filter` pre-allocates: `make([]T, 0, len(values))` ✅ (Fixed 2025-11-14)
|
||||||
|
- `Map` pre-allocates: `make([]P, 0, len(values))`
|
||||||
|
- `Partition` pre-allocates: `make([]T, 0, len(values))` for both slices ✅ (Fixed 2025-11-14)
|
||||||
|
- `Chunk` pre-calculates capacity: `(l+n-1)/n`
|
||||||
|
- `ParallelFilter` pre-counts before allocation
|
||||||
|
- `OrderBy` uses `slices.SortFunc`: O(n log n) performance ✅ (Fixed 2025-11-14)
|
||||||
|
|
||||||
|
### Remaining Performance Issues
|
||||||
|
- `Flatmap`: Accumulation overhead from repeated appends
|
||||||
|
- `GroupBy`: Map initialized with capacity 0 (useless hint)
|
||||||
|
|
||||||
|
### When to Use ParallelMap vs Map
|
||||||
|
|
||||||
|
Use `ParallelMap` when:
|
||||||
|
- Processing 100+ elements with expensive operations (>1ms per element)
|
||||||
|
- Operations are CPU-bound (not I/O-bound with shared resources)
|
||||||
|
- Order preservation is required
|
||||||
|
- Context cancellation is needed
|
||||||
|
|
||||||
|
Use regular `Map` when:
|
||||||
|
- Small slices (<100 elements)
|
||||||
|
- Fast operations (<100µs per element)
|
||||||
|
- Avoiding goroutine overhead matters
|
||||||
|
- Simple transformations without error handling
|
||||||
|
|
||||||
|
**Worker count guidelines:**
|
||||||
|
- Default (workers=0): Uses `runtime.GOMAXPROCS(0)` - good starting point
|
||||||
|
- CPU-bound: Use GOMAXPROCS or GOMAXPROCS*2
|
||||||
|
- I/O-bound: Can use higher values (10-100) if not sharing resources
|
||||||
|
|
||||||
|
## Contributing Notes
|
||||||
|
|
||||||
|
When adding new functions:
|
||||||
|
|
||||||
|
1. Create both `<function>.go` and `<function>_test.go`
|
||||||
|
2. Add examples in comments using Go doc format
|
||||||
|
3. Pre-allocate slices with `make([]T, 0, len(input))` when output size is similar to input
|
||||||
|
4. Document panic conditions (empty slices, nil inputs, invalid indices)
|
||||||
|
5. Add edge case tests (empty, single element, nil)
|
||||||
|
6. If the function applies to Pipe chains, add a method to `pipe.go`
|
||||||
|
7. Update README.md function list if adding new collection functions
|
||||||
|
8. Follow SemVer for version numbers
|
||||||
|
9. Ensure all tests pass: `make test`
|
||||||
|
|
||||||
|
When fixing bugs:
|
||||||
|
- Add regression tests before fixing
|
||||||
|
- Run benchmarks if performance-related: `go test -bench=. -benchmem`
|
||||||
|
- Check for similar issues in other functions
|
||||||
28
README.md
28
README.md
|
|
@ -21,7 +21,7 @@ It is mostly a port from the `underscore.js` library based on generics brought b
|
||||||
Install the library using
|
Install the library using
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get github.com/rjNemo/underscore@0.7.0
|
go get github.com/rjNemo/underscore@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Please check out the [examples](examples) to see how to use the library.
|
Please check out the [examples](examples) to see how to use the library.
|
||||||
|
|
@ -92,22 +92,33 @@ make test
|
||||||
|
|
||||||
- `All`
|
- `All`
|
||||||
- `Any`
|
- `Any`
|
||||||
|
- `Chunk`
|
||||||
- `Contains`
|
- `Contains`
|
||||||
- `ContainsBy`
|
- `ContainsBy`
|
||||||
|
- `Count`
|
||||||
|
- `Difference`
|
||||||
|
- `Drop`
|
||||||
- `Each`
|
- `Each`
|
||||||
- `Filter`
|
- `Filter`
|
||||||
|
- `Find`
|
||||||
- `Flatmap`
|
- `Flatmap`
|
||||||
- `GroupBy`
|
- `GroupBy`
|
||||||
- `Find`
|
- `Intersection`
|
||||||
|
- `Join` / `JoinProject`
|
||||||
|
- `Last`
|
||||||
- `Map`
|
- `Map`
|
||||||
- `Max`
|
- `Max`
|
||||||
- `Min`
|
- `Min`
|
||||||
|
- `OrderBy`
|
||||||
- `Partition`
|
- `Partition`
|
||||||
|
- `Range`
|
||||||
- `Reduce`
|
- `Reduce`
|
||||||
|
- `RemoveAt`
|
||||||
|
- `Sum` / `SumMap`
|
||||||
- `Unique`
|
- `Unique`
|
||||||
- `UniqueBy`
|
- `UniqueBy`
|
||||||
- `UniqueInPlace`
|
- `UniqueInPlace`
|
||||||
- `Chunk`
|
- `Zip`
|
||||||
|
|
||||||
### Pipe
|
### Pipe
|
||||||
|
|
||||||
|
|
@ -157,9 +168,18 @@ func main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|
- `Ternary`: conditional expression helper
|
||||||
|
- `ToPointer`: convert values to pointers
|
||||||
|
- `SortSliceASC` / `SortSliceDESC`: sort slices in ascending or descending order
|
||||||
|
- `Result`, `Ok`, `Err`, `ToResult`: Result type for error handling
|
||||||
|
- `Tuple`: generic tuple type for paired values
|
||||||
|
|
||||||
### Subpackages
|
### Subpackages
|
||||||
|
|
||||||
- `maps.Keys(m)` / `maps.Values(m)`: utilities to extract keys or values from maps.
|
- `maps.Keys(m)` / `maps.Values(m)`: extract keys or values from maps
|
||||||
|
- `maps.Map(m, fn)`: transform map entries
|
||||||
|
|
||||||
## Built With
|
## Built With
|
||||||
|
|
||||||
|
|
|
||||||
25
docs/content/collections/dropwhile.md
Normal file
25
docs/content/collections/dropwhile.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: "DropWhile"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`DropWhile` drops elements from the beginning of the slice while the predicate returns true. It returns the remaining elements starting from the first element where the predicate returns false.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||||
|
lessThan5 := func(n int) bool { return n < 5 }
|
||||||
|
fmt.Println(u.DropWhile(nums, lessThan5)) // [5, 6, 7, 8, 9]
|
||||||
|
|
||||||
|
words := []string{"apple", "banana", "cherry", "date"}
|
||||||
|
shortWords := func(s string) bool { return len(s) < 6 }
|
||||||
|
fmt.Println(u.DropWhile(words, shortWords)) // ["banana", "cherry", "date"]
|
||||||
|
}
|
||||||
|
```
|
||||||
31
docs/content/collections/first.md
Normal file
31
docs/content/collections/first.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
title: "First"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`First` returns the first element of the slice. Returns an error if the slice is empty.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
first, err := u.First(nums)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(first) // 1
|
||||||
|
|
||||||
|
// Handle empty slice
|
||||||
|
empty := []int{}
|
||||||
|
_, err = u.First(empty)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err) // Error: underscore: empty slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/content/collections/firstn.md
Normal file
23
docs/content/collections/firstn.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
title: "FirstN"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`FirstN` returns the first n elements of the slice. If n is greater than the slice length, returns the entire slice. If n is less than or equal to 0, returns an empty slice.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||||
|
fmt.Println(u.FirstN(nums, 3)) // [1, 2, 3]
|
||||||
|
fmt.Println(u.FirstN(nums, 0)) // []
|
||||||
|
fmt.Println(u.FirstN(nums, 10)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
|
fmt.Println(u.FirstN(nums, -5)) // []
|
||||||
|
}
|
||||||
|
```
|
||||||
39
docs/content/collections/foldright.md
Normal file
39
docs/content/collections/foldright.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
title: "FoldRight"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`FoldRight` is like Reduce but processes elements from right to left. Also known as foldr in Haskell. Important for non-associative operations where the order of evaluation matters.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Subtraction is non-associative
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
|
||||||
|
// FoldRight: 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2
|
||||||
|
result := u.FoldRight(nums, 0, func(n, acc int) int { return n - acc })
|
||||||
|
fmt.Println(result) // 2
|
||||||
|
|
||||||
|
// Compare with Reduce (left fold): (0 - 1) - 2 - 3 = -6
|
||||||
|
leftResult := u.Reduce(nums, func(n, acc int) int { return acc - n }, 0)
|
||||||
|
fmt.Println(leftResult) // -6
|
||||||
|
|
||||||
|
// Building a list in order
|
||||||
|
buildList := u.FoldRight(nums, []int{}, func(n int, acc []int) []int {
|
||||||
|
return append([]int{n}, acc...)
|
||||||
|
})
|
||||||
|
fmt.Println(buildList) // [1, 2, 3]
|
||||||
|
|
||||||
|
// String concatenation
|
||||||
|
words := []string{"a", "b", "c"}
|
||||||
|
concat := u.FoldRight(words, "", func(s, acc string) string { return s + acc })
|
||||||
|
fmt.Println(concat) // "abc"
|
||||||
|
}
|
||||||
|
```
|
||||||
32
docs/content/collections/init.md
Normal file
32
docs/content/collections/init.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
title: "Init"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Init` returns all elements except the last one, and the last element separately. Returns an empty slice and zero value if the input slice is empty. Useful for destructuring lists from the right.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
init, last := u.Init(nums)
|
||||||
|
fmt.Println(init) // [1, 2, 3, 4]
|
||||||
|
fmt.Println(last) // 5
|
||||||
|
|
||||||
|
// Single element
|
||||||
|
single, val := u.Init([]int{42})
|
||||||
|
fmt.Println(single) // []
|
||||||
|
fmt.Println(val) // 42
|
||||||
|
|
||||||
|
// Empty slice
|
||||||
|
empty, zero := u.Init([]int{})
|
||||||
|
fmt.Println(empty) // []
|
||||||
|
fmt.Println(zero) // 0
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/content/collections/intersperse.md
Normal file
28
docs/content/collections/intersperse.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
title: "Intersperse"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Intersperse` inserts a separator between each element of the slice. Returns an empty slice if the input is empty. Returns the original element if the input has only one element.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
fmt.Println(u.Intersperse(nums, 0)) // [1, 0, 2, 0, 3, 0, 4, 0, 5]
|
||||||
|
|
||||||
|
// Useful for formatting
|
||||||
|
words := []string{"apple", "banana", "cherry"}
|
||||||
|
fmt.Println(u.Intersperse(words, ",")) // ["apple", ",", "banana", ",", "cherry"]
|
||||||
|
|
||||||
|
// Single element - no separator added
|
||||||
|
single := []int{42}
|
||||||
|
fmt.Println(u.Intersperse(single, 0)) // [42]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -3,7 +3,7 @@ title: "Last"
|
||||||
date: 2022-03-21T13:46:24-04:00
|
date: 2022-03-21T13:46:24-04:00
|
||||||
---
|
---
|
||||||
|
|
||||||
`Last` returns the last element of the slice.
|
`Last` returns the last element of the slice. Panics if the slice is empty with a clear error message.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
@ -15,7 +15,14 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||||
|
|
||||||
fmt.Println(u.Last(nums)) // 5
|
fmt.Println(u.Last(nums)) // 5
|
||||||
|
|
||||||
|
// Single element
|
||||||
|
single := []int{42}
|
||||||
|
fmt.Println(u.Last(single)) // 42
|
||||||
|
|
||||||
|
// Empty slice panics with clear message
|
||||||
|
// empty := []int{}
|
||||||
|
// u.Last(empty) // panic: underscore.Last: empty slice
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
51
docs/content/collections/parallel_reduce.md
Normal file
51
docs/content/collections/parallel_reduce.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
title: "ParallelReduce"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`ParallelReduce` applies a reduction function in parallel using a worker pool. The operation must be associative and commutative for correct results. If workers <= 0, defaults to GOMAXPROCS. On error, the first error is returned and processing is canceled.
|
||||||
|
|
||||||
|
**Note:** This is an experimental function. Order of operations is not guaranteed, so use only with associative and commutative operations (like addition, multiplication, min, max).
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Parallel sum (safe - addition is associative and commutative)
|
||||||
|
result, err := u.ParallelReduce(ctx, nums, 4, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
// Simulate expensive computation
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(result) // Result will vary due to parallel execution
|
||||||
|
|
||||||
|
// With context cancellation
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err = u.ParallelReduce(ctx, nums, 4, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Operation was cancelled:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warning:** Do not use ParallelReduce for non-associative operations like subtraction or division, as the results will be unpredictable due to parallel execution order.
|
||||||
43
docs/content/collections/replicate.md
Normal file
43
docs/content/collections/replicate.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
title: "Replicate"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Replicate` creates a slice containing count copies of value. Returns an empty slice if count is less than or equal to 0. Useful for initialization and testing.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Basic usage
|
||||||
|
fmt.Println(u.Replicate(3, "hello"))
|
||||||
|
// ["hello", "hello", "hello"]
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
fmt.Println(u.Replicate(5, 0))
|
||||||
|
// [0, 0, 0, 0, 0]
|
||||||
|
|
||||||
|
// Zero count
|
||||||
|
fmt.Println(u.Replicate(0, 42))
|
||||||
|
// []
|
||||||
|
|
||||||
|
// Negative count
|
||||||
|
fmt.Println(u.Replicate(-5, "x"))
|
||||||
|
// []
|
||||||
|
|
||||||
|
// Use case: initialize with default values
|
||||||
|
defaultScores := u.Replicate(10, 100)
|
||||||
|
fmt.Println(defaultScores)
|
||||||
|
// [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]
|
||||||
|
|
||||||
|
// Use case: creating separators
|
||||||
|
separator := u.Replicate(40, "-")
|
||||||
|
fmt.Println(u.Reduce(separator, func(s, acc string) string { return acc + s }, ""))
|
||||||
|
// ----------------------------------------
|
||||||
|
}
|
||||||
|
```
|
||||||
37
docs/content/collections/scan.md
Normal file
37
docs/content/collections/scan.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
title: "Scan"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Scan` is like Reduce but returns all intermediate accumulator values. Also known as prefix scan or cumulative fold. Useful for tracking running totals, running maximums, or other cumulative operations.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Running sum
|
||||||
|
nums := []int{1, 2, 3, 4}
|
||||||
|
add := func(acc, n int) int { return acc + n }
|
||||||
|
fmt.Println(u.Scan(nums, 0, add)) // [1, 3, 6, 10]
|
||||||
|
|
||||||
|
// Running maximum
|
||||||
|
values := []int{3, 1, 4, 1, 5, 9, 2}
|
||||||
|
max := func(acc, n int) int {
|
||||||
|
if n > acc {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
fmt.Println(u.Scan(values, 0, max)) // [3, 3, 4, 4, 5, 9, 9]
|
||||||
|
|
||||||
|
// String concatenation
|
||||||
|
words := []string{"hello", "world", "!"}
|
||||||
|
concat := func(acc, s string) string { return acc + s }
|
||||||
|
fmt.Println(u.Scan(words, "", concat)) // ["hello", "helloworld", "helloworld!"]
|
||||||
|
}
|
||||||
|
```
|
||||||
43
docs/content/collections/sliding.md
Normal file
43
docs/content/collections/sliding.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
title: "Sliding"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Sliding` creates a sliding window view of the slice with the specified window size. Returns an empty slice if size is less than or equal to 0 or greater than the slice length. Useful for moving averages, n-grams, and pattern matching.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
fmt.Println(u.Sliding(nums, 3)) // [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
|
||||||
|
|
||||||
|
// Size 2
|
||||||
|
fmt.Println(u.Sliding(nums, 2)) // [[1, 2], [2, 3], [3, 4], [4, 5]]
|
||||||
|
|
||||||
|
// N-grams for text
|
||||||
|
words := []string{"the", "quick", "brown", "fox"}
|
||||||
|
bigrams := u.Sliding(words, 2)
|
||||||
|
fmt.Println(bigrams) // [["the", "quick"], ["quick", "brown"], ["brown", "fox"]]
|
||||||
|
|
||||||
|
// Moving average example
|
||||||
|
data := []int{10, 20, 30, 40, 50}
|
||||||
|
windows := u.Sliding(data, 3)
|
||||||
|
for _, window := range windows {
|
||||||
|
sum := 0
|
||||||
|
for _, v := range window {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
avg := sum / len(window)
|
||||||
|
fmt.Printf("Window: %v, Average: %d\n", window, avg)
|
||||||
|
}
|
||||||
|
// Window: [10 20 30], Average: 20
|
||||||
|
// Window: [20 30 40], Average: 30
|
||||||
|
// Window: [30 40 50], Average: 40
|
||||||
|
}
|
||||||
|
```
|
||||||
25
docs/content/collections/takewhile.md
Normal file
25
docs/content/collections/takewhile.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: "TakeWhile"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`TakeWhile` returns elements from the beginning of the slice while the predicate returns true. It stops at the first element where the predicate returns false.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||||
|
lessThan5 := func(n int) bool { return n < 5 }
|
||||||
|
fmt.Println(u.TakeWhile(nums, lessThan5)) // [1, 2, 3, 4]
|
||||||
|
|
||||||
|
words := []string{"apple", "banana", "cherry", "date"}
|
||||||
|
shortWords := func(s string) bool { return len(s) < 6 }
|
||||||
|
fmt.Println(u.TakeWhile(words, shortWords)) // ["apple"]
|
||||||
|
}
|
||||||
|
```
|
||||||
47
docs/content/collections/tap.md
Normal file
47
docs/content/collections/tap.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
title: "Tap"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Tap` applies a function to each element for side effects (like debugging or logging) and returns the original slice unchanged. Useful for debugging pipelines without breaking the flow.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Debugging a pipeline
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
|
||||||
|
result := u.Tap(
|
||||||
|
u.Map(
|
||||||
|
u.Filter(nums, func(n int) bool { return n%2 == 0 }),
|
||||||
|
func(n int) int { return n * 2 },
|
||||||
|
),
|
||||||
|
func(n int) {
|
||||||
|
fmt.Printf("Debug: %d\n", n) // Prints each value
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println(result) // [4, 8]
|
||||||
|
|
||||||
|
// Counting elements that pass through
|
||||||
|
count := 0
|
||||||
|
filtered := u.Tap(
|
||||||
|
u.Filter(nums, func(n int) bool { return n > 2 }),
|
||||||
|
func(n int) { count++ },
|
||||||
|
)
|
||||||
|
fmt.Printf("Found %d elements: %v\n", count, filtered)
|
||||||
|
// Found 3 elements: [3 4 5]
|
||||||
|
|
||||||
|
// Logging transformations
|
||||||
|
data := []string{"hello", "world"}
|
||||||
|
u.Tap(data, func(s string) {
|
||||||
|
fmt.Printf("Processing: %s\n", s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
45
docs/content/collections/transpose.md
Normal file
45
docs/content/collections/transpose.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
title: "Transpose"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Transpose` flips a matrix over its diagonal, swapping rows and columns. Returns an empty slice if the input is empty. Assumes all rows have the same length (uses the length of the first row).
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 2x3 matrix becomes 3x2 matrix
|
||||||
|
matrix := [][]int{
|
||||||
|
{1, 2, 3},
|
||||||
|
{4, 5, 6},
|
||||||
|
}
|
||||||
|
transposed := u.Transpose(matrix)
|
||||||
|
fmt.Println(transposed)
|
||||||
|
// [[1, 4], [2, 5], [3, 6]]
|
||||||
|
|
||||||
|
// Square matrix
|
||||||
|
square := [][]int{
|
||||||
|
{1, 2},
|
||||||
|
{3, 4},
|
||||||
|
}
|
||||||
|
fmt.Println(u.Transpose(square))
|
||||||
|
// [[1, 3], [2, 4]]
|
||||||
|
|
||||||
|
// Use case: converting rows to columns for processing
|
||||||
|
data := [][]string{
|
||||||
|
{"Name", "Age", "City"},
|
||||||
|
{"Alice", "30", "NYC"},
|
||||||
|
{"Bob", "25", "LA"},
|
||||||
|
}
|
||||||
|
byColumn := u.Transpose(data)
|
||||||
|
fmt.Println("Names:", byColumn[0]) // [Name Alice Bob]
|
||||||
|
fmt.Println("Ages:", byColumn[1]) // [Age 30 25]
|
||||||
|
fmt.Println("Cities:", byColumn[2]) // [City NYC LA]
|
||||||
|
}
|
||||||
|
```
|
||||||
43
docs/content/collections/unzip.md
Normal file
43
docs/content/collections/unzip.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
title: "Unzip"
|
||||||
|
date: 2025-01-16T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Unzip` splits a slice of tuples into two separate slices. The inverse operation of Zip. Useful for separating paired data.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Basic usage
|
||||||
|
pairs := []u.Tuple[int, string]{
|
||||||
|
{Left: 1, Right: "a"},
|
||||||
|
{Left: 2, Right: "b"},
|
||||||
|
{Left: 3, Right: "c"},
|
||||||
|
}
|
||||||
|
|
||||||
|
nums, letters := u.Unzip(pairs)
|
||||||
|
fmt.Println(nums) // [1, 2, 3]
|
||||||
|
fmt.Println(letters) // ["a", "b", "c"]
|
||||||
|
|
||||||
|
// Use case: separating keys and values
|
||||||
|
keyValuePairs := []u.Tuple[string, int]{
|
||||||
|
{Left: "apple", Right: 5},
|
||||||
|
{Left: "banana", Right: 3},
|
||||||
|
{Left: "cherry", Right: 8},
|
||||||
|
}
|
||||||
|
|
||||||
|
items, counts := u.Unzip(keyValuePairs)
|
||||||
|
fmt.Println("Items:", items) // Items: [apple banana cherry]
|
||||||
|
fmt.Println("Counts:", counts) // Counts: [5 3 8]
|
||||||
|
|
||||||
|
// Empty slice
|
||||||
|
emptyNums, emptyStrs := u.Unzip([]u.Tuple[int, string]{})
|
||||||
|
fmt.Println(emptyNums, emptyStrs) // [] []
|
||||||
|
}
|
||||||
|
```
|
||||||
18
drop.go
18
drop.go
|
|
@ -1,12 +1,16 @@
|
||||||
package underscore
|
package underscore
|
||||||
|
|
||||||
// Drop returns the rest of the elements in a slice.
|
// Drop returns a new slice with the first n elements removed.
|
||||||
// Pass an index to return the values of the slice from that index onward.
|
// If n is greater than or equal to the slice length, returns an empty slice.
|
||||||
func Drop[T any](values []T, index int) (rest []T) {
|
// If n is less than or equal to 0, returns the original slice.
|
||||||
for i, value := range values {
|
func Drop[T any](values []T, n int) []T {
|
||||||
if i != index {
|
if n <= 0 {
|
||||||
rest = append(rest, value)
|
return values
|
||||||
}
|
}
|
||||||
|
if n >= len(values) {
|
||||||
|
return []T{}
|
||||||
}
|
}
|
||||||
return rest
|
res := make([]T, len(values)-n)
|
||||||
|
copy(res, values[n:])
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
drop_test.go
32
drop_test.go
|
|
@ -9,8 +9,34 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDrop(t *testing.T) {
|
func TestDrop(t *testing.T) {
|
||||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
want := []int{1, 9, 2, 3, 7, 4, 6, 5}
|
want := []int{3, 4, 5}
|
||||||
|
|
||||||
assert.Equal(t, want, u.Drop(nums, 3))
|
assert.Equal(t, want, u.Drop(nums, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropNone(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
|
||||||
|
assert.Equal(t, nums, u.Drop(nums, 0))
|
||||||
|
assert.Equal(t, nums, u.Drop(nums, -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropAll(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
|
||||||
|
assert.Empty(t, u.Drop(nums, 5))
|
||||||
|
assert.Empty(t, u.Drop(nums, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropEmpty(t *testing.T) {
|
||||||
|
result := u.Drop([]int{}, 5)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropSingleElement(t *testing.T) {
|
||||||
|
nums := []int{42}
|
||||||
|
|
||||||
|
assert.Equal(t, nums, u.Drop(nums, 0))
|
||||||
|
assert.Empty(t, u.Drop(nums, 1))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
dropwhile.go
Normal file
15
dropwhile.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// DropWhile drops elements from the beginning of the slice while the predicate returns true.
|
||||||
|
// It returns the remaining elements starting from the first element where the predicate returns false.
|
||||||
|
func DropWhile[T any](values []T, predicate func(T) bool) []T {
|
||||||
|
for i, v := range values {
|
||||||
|
if !predicate(v) {
|
||||||
|
res := make([]T, len(values)-i)
|
||||||
|
copy(res, values[i:])
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All elements satisfy predicate, return empty slice
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
55
dropwhile_test.go
Normal file
55
dropwhile_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDropWhile(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||||
|
result := u.DropWhile(nums, func(n int) bool { return n < 5 })
|
||||||
|
assert.Equal(t, []int{5, 6, 7, 8, 9}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropWhileEmpty(t *testing.T) {
|
||||||
|
result := u.DropWhile([]int{}, func(n int) bool { return n < 5 })
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropWhileNoneMatch(t *testing.T) {
|
||||||
|
nums := []int{5, 6, 7, 8, 9}
|
||||||
|
result := u.DropWhile(nums, func(n int) bool { return n < 5 })
|
||||||
|
assert.Equal(t, []int{5, 6, 7, 8, 9}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropWhileAllMatch(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4}
|
||||||
|
result := u.DropWhile(nums, func(n int) bool { return n < 10 })
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropWhileSingleElement(t *testing.T) {
|
||||||
|
result := u.DropWhile([]int{5}, func(n int) bool { return n < 10 })
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropWhileStrings(t *testing.T) {
|
||||||
|
words := []string{"apple", "banana", "cherry", "date"}
|
||||||
|
result := u.DropWhile(words, func(s string) bool { return len(s) < 6 })
|
||||||
|
assert.Equal(t, []string{"banana", "cherry", "date"}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDropWhile(b *testing.B) {
|
||||||
|
nums := make([]int, 1000)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.DropWhile(nums, func(n int) bool { return n < 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package underscore
|
||||||
|
|
||||||
// Filter looks through each value in the slice, returning a slice of all the values that pass a truth test (predicate).
|
// Filter looks through each value in the slice, returning a slice of all the values that pass a truth test (predicate).
|
||||||
func Filter[T any](values []T, predicate func(T) bool) (res []T) {
|
func Filter[T any](values []T, predicate func(T) bool) (res []T) {
|
||||||
|
res = make([]T, 0, len(values))
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
if predicate(v) {
|
if predicate(v) {
|
||||||
res = append(res, v)
|
res = append(res, v)
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,40 @@ func TestFilter(t *testing.T) {
|
||||||
want := []int{0, 2, 4, 6, 8}
|
want := []int{0, 2, 4, 6, 8}
|
||||||
assert.Equal(t, want, u.Filter(nums, isEven))
|
assert.Equal(t, want, u.Filter(nums, isEven))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterEmpty(t *testing.T) {
|
||||||
|
result := u.Filter([]int{}, func(n int) bool { return n > 0 })
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterSingleElement(t *testing.T) {
|
||||||
|
result := u.Filter([]int{5}, func(n int) bool { return n > 0 })
|
||||||
|
assert.Equal(t, []int{5}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterSingleElementNoMatch(t *testing.T) {
|
||||||
|
result := u.Filter([]int{5}, func(n int) bool { return n > 10 })
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterLarge(t *testing.T) {
|
||||||
|
large := make([]int, 10000)
|
||||||
|
for i := range large {
|
||||||
|
large[i] = i
|
||||||
|
}
|
||||||
|
result := u.Filter(large, func(n int) bool { return n%2 == 0 })
|
||||||
|
assert.Equal(t, 5000, len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFilter(b *testing.B) {
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
isEven := func(n int) bool { return n%2 == 0 }
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Filter(data, isEven)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
33
first.go
Normal file
33
first.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// ErrEmptySlice is returned when trying to get the first element of an empty slice
|
||||||
|
var ErrEmptySlice = errors.New("underscore: empty slice")
|
||||||
|
|
||||||
|
// First returns the first element of the slice.
|
||||||
|
// Returns an error if the slice is empty.
|
||||||
|
func First[T any](values []T) (T, error) {
|
||||||
|
var zero T
|
||||||
|
if len(values) == 0 {
|
||||||
|
return zero, ErrEmptySlice
|
||||||
|
}
|
||||||
|
return values[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstN returns the first n elements of the slice.
|
||||||
|
// If n is greater than the slice length, returns the entire slice.
|
||||||
|
// If n is less than or equal to 0, returns an empty slice.
|
||||||
|
func FirstN[T any](values []T, n int) []T {
|
||||||
|
if n <= 0 {
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
if n >= len(values) {
|
||||||
|
res := make([]T, len(values))
|
||||||
|
copy(res, values)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
res := make([]T, n)
|
||||||
|
copy(res, values[:n])
|
||||||
|
return res
|
||||||
|
}
|
||||||
97
first_test.go
Normal file
97
first_test.go
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFirst(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
result, err := u.First(nums)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstEmpty(t *testing.T) {
|
||||||
|
_, err := u.First([]int{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, errors.Is(err, u.ErrEmptySlice))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstSingleElement(t *testing.T) {
|
||||||
|
result, err := u.First([]int{42})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 42, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstStrings(t *testing.T) {
|
||||||
|
words := []string{"hello", "world"}
|
||||||
|
result, err := u.First(words)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstN(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
result := u.FirstN(nums, 3)
|
||||||
|
assert.Equal(t, []int{1, 2, 3}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstNEmpty(t *testing.T) {
|
||||||
|
result := u.FirstN([]int{}, 3)
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstNZero(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.FirstN(nums, 0)
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstNNegative(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.FirstN(nums, -5)
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstNGreaterThanLength(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.FirstN(nums, 10)
|
||||||
|
assert.Equal(t, []int{1, 2, 3}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstNSingleElement(t *testing.T) {
|
||||||
|
result := u.FirstN([]int{42}, 1)
|
||||||
|
assert.Equal(t, []int{42}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstNAll(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.FirstN(nums, 3)
|
||||||
|
assert.Equal(t, []int{1, 2, 3}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFirst(b *testing.B) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = u.First(nums)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFirstN(b *testing.B) {
|
||||||
|
nums := make([]int, 1000)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.FirstN(nums, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,8 @@ package underscore
|
||||||
|
|
||||||
// Flatmap flatten the input slice element into the new slice. FlatMap maps every element with the help of a mapper function, then flattens the input slice element into the new slice.
|
// Flatmap flatten the input slice element into the new slice. FlatMap maps every element with the help of a mapper function, then flattens the input slice element into the new slice.
|
||||||
func Flatmap[T any](values []T, mapper func(n T) []T) []T {
|
func Flatmap[T any](values []T, mapper func(n T) []T) []T {
|
||||||
res := make([]T, 0)
|
// Estimate capacity: assume average of 2-3 items per element
|
||||||
|
res := make([]T, 0, len(values)*2)
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
vs := mapper(v)
|
vs := mapper(v)
|
||||||
res = append(res, vs...)
|
res = append(res, vs...)
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,16 @@ func TestFlatmap(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, want, u.Flatmap(nums, transform))
|
assert.Equal(t, want, u.Flatmap(nums, transform))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkFlatmap(b *testing.B) {
|
||||||
|
data := make([]int, 100)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
mapper := func(n int) []int { return []int{n, n * 2, n * 3} }
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Flatmap(data, mapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
14
foldright.go
Normal file
14
foldright.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// FoldRight is like Reduce but processes elements from right to left.
|
||||||
|
// Also known as foldr in Haskell.
|
||||||
|
//
|
||||||
|
// Example: FoldRight([]int{1,2,3}, 0, func(n, acc int) int { return n - acc })
|
||||||
|
//
|
||||||
|
// → 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2
|
||||||
|
func FoldRight[T, P any](values []T, acc P, fn func(T, P) P) P {
|
||||||
|
for i := len(values) - 1; i >= 0; i-- {
|
||||||
|
acc = fn(values[i], acc)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}
|
||||||
80
foldright_test.go
Normal file
80
foldright_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFoldRight(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4}
|
||||||
|
result := u.FoldRight(nums, 0, func(n, acc int) int { return n + acc })
|
||||||
|
assert.Equal(t, 10, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRightEmpty(t *testing.T) {
|
||||||
|
result := u.FoldRight([]int{}, 42, func(n, acc int) int { return n + acc })
|
||||||
|
assert.Equal(t, 42, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRightSingleElement(t *testing.T) {
|
||||||
|
result := u.FoldRight([]int{5}, 0, func(n, acc int) int { return n + acc })
|
||||||
|
assert.Equal(t, 5, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRightSubtraction(t *testing.T) {
|
||||||
|
// FoldRight: 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.FoldRight(nums, 0, func(n, acc int) int { return n - acc })
|
||||||
|
assert.Equal(t, 2, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRightDivision(t *testing.T) {
|
||||||
|
// FoldRight with float: 2.0 / (4.0 / (8.0 / 1.0)) = 2.0 / (4.0 / 8.0) = 2.0 / 0.5 = 4.0
|
||||||
|
nums := []float64{2.0, 4.0, 8.0}
|
||||||
|
result := u.FoldRight(nums, 1.0, func(n, acc float64) float64 { return n / acc })
|
||||||
|
assert.Equal(t, 4.0, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRightStrings(t *testing.T) {
|
||||||
|
words := []string{"a", "b", "c"}
|
||||||
|
result := u.FoldRight(words, "", func(s, acc string) string { return s + acc })
|
||||||
|
assert.Equal(t, "abc", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRightVsReduce(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
|
||||||
|
// Reduce (left fold): (0 - 1) - 2 - 3 = -6
|
||||||
|
reduceResult := u.Reduce(nums, func(n, acc int) int { return acc - n }, 0)
|
||||||
|
assert.Equal(t, -6, reduceResult)
|
||||||
|
|
||||||
|
// FoldRight: 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2
|
||||||
|
foldRightResult := u.FoldRight(nums, 0, func(n, acc int) int { return n - acc })
|
||||||
|
assert.Equal(t, 2, foldRightResult)
|
||||||
|
|
||||||
|
// They should be different for non-associative operations
|
||||||
|
assert.NotEqual(t, reduceResult, foldRightResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRightBuildList(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.FoldRight(nums, []int{}, func(n int, acc []int) []int {
|
||||||
|
return append([]int{n}, acc...)
|
||||||
|
})
|
||||||
|
assert.Equal(t, []int{1, 2, 3}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFoldRight(b *testing.B) {
|
||||||
|
nums := make([]int, 1000)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.FoldRight(nums, 0, func(n, acc int) int { return n + acc })
|
||||||
|
}
|
||||||
|
}
|
||||||
5
go.mod
5
go.mod
|
|
@ -2,10 +2,7 @@ module github.com/rjNemo/underscore
|
||||||
|
|
||||||
go 1.24.2
|
go 1.24.2
|
||||||
|
|
||||||
require (
|
require github.com/stretchr/testify v1.8.4
|
||||||
github.com/stretchr/testify v1.8.4
|
|
||||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -4,8 +4,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
|
|
||||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package underscore
|
||||||
|
|
||||||
// GroupBy splits a slice into a map[K][]V grouped by the result of the iterator function.
|
// GroupBy splits a slice into a map[K][]V grouped by the result of the iterator function.
|
||||||
func GroupBy[K comparable, V any](values []V, f func(V) K) map[K][]V {
|
func GroupBy[K comparable, V any](values []V, f func(V) K) map[K][]V {
|
||||||
res := make(map[K][]V, 0)
|
res := make(map[K][]V, len(values)/10)
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
k := f(v)
|
k := f(v)
|
||||||
if r, ok := res[k]; ok {
|
if r, ok := res[k]; ok {
|
||||||
|
|
|
||||||
18
init.go
Normal file
18
init.go
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Init returns all elements except the last one, and the last element separately.
|
||||||
|
// Returns an empty slice and zero value if the input slice is empty.
|
||||||
|
// Also known as "uncons from the right" or "snoc" inverse.
|
||||||
|
func Init[T any](values []T) ([]T, T) {
|
||||||
|
var last T
|
||||||
|
if len(values) == 0 {
|
||||||
|
return []T{}, last
|
||||||
|
}
|
||||||
|
if len(values) == 1 {
|
||||||
|
return []T{}, values[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]T, len(values)-1)
|
||||||
|
copy(res, values[:len(values)-1])
|
||||||
|
return res, values[len(values)-1]
|
||||||
|
}
|
||||||
65
init_test.go
Normal file
65
init_test.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
init, last := u.Init(nums)
|
||||||
|
assert.Equal(t, []int{1, 2, 3, 4}, init)
|
||||||
|
assert.Equal(t, 5, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitEmpty(t *testing.T) {
|
||||||
|
init, last := u.Init([]int{})
|
||||||
|
assert.Equal(t, []int{}, init)
|
||||||
|
assert.Equal(t, 0, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitSingleElement(t *testing.T) {
|
||||||
|
init, last := u.Init([]int{42})
|
||||||
|
assert.Equal(t, []int{}, init)
|
||||||
|
assert.Equal(t, 42, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitTwoElements(t *testing.T) {
|
||||||
|
init, last := u.Init([]int{1, 2})
|
||||||
|
assert.Equal(t, []int{1}, init)
|
||||||
|
assert.Equal(t, 2, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitStrings(t *testing.T) {
|
||||||
|
words := []string{"hello", "world", "!"}
|
||||||
|
init, last := u.Init(words)
|
||||||
|
assert.Equal(t, []string{"hello", "world"}, init)
|
||||||
|
assert.Equal(t, "!", last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitDoesNotMutate(t *testing.T) {
|
||||||
|
original := []int{1, 2, 3, 4, 5}
|
||||||
|
init, last := u.Init(original)
|
||||||
|
|
||||||
|
// Modify returned slice
|
||||||
|
init[0] = 999
|
||||||
|
|
||||||
|
// Original should be unchanged
|
||||||
|
assert.Equal(t, 1, original[0])
|
||||||
|
assert.Equal(t, 5, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInit(b *testing.B) {
|
||||||
|
nums := make([]int, 1000)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Init(nums)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
intersperse.go
Normal file
23
intersperse.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Intersperse inserts a separator between each element of the slice.
|
||||||
|
// Returns an empty slice if the input is empty.
|
||||||
|
// Returns the original element if the input has only one element.
|
||||||
|
//
|
||||||
|
// Example: Intersperse([]int{1,2,3}, 0) → [1, 0, 2, 0, 3]
|
||||||
|
func Intersperse[T any](values []T, separator T) []T {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
if len(values) == 1 {
|
||||||
|
return []T{values[0]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result will have len(values) + (len(values)-1) elements
|
||||||
|
res := make([]T, 0, len(values)*2-1)
|
||||||
|
res = append(res, values[0])
|
||||||
|
for i := 1; i < len(values); i++ {
|
||||||
|
res = append(res, separator, values[i])
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
60
intersperse_test.go
Normal file
60
intersperse_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntersperse(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
result := u.Intersperse(nums, 0)
|
||||||
|
assert.Equal(t, []int{1, 0, 2, 0, 3, 0, 4, 0, 5}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntersperseEmpty(t *testing.T) {
|
||||||
|
result := u.Intersperse([]int{}, 0)
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntersperseSingleElement(t *testing.T) {
|
||||||
|
result := u.Intersperse([]int{42}, 0)
|
||||||
|
assert.Equal(t, []int{42}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntersperseTwoElements(t *testing.T) {
|
||||||
|
result := u.Intersperse([]int{1, 2}, 0)
|
||||||
|
assert.Equal(t, []int{1, 0, 2}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntersperseStrings(t *testing.T) {
|
||||||
|
words := []string{"hello", "world", "!"}
|
||||||
|
result := u.Intersperse(words, ",")
|
||||||
|
assert.Equal(t, []string{"hello", ",", "world", ",", "!"}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntersperseComma(t *testing.T) {
|
||||||
|
words := []string{"apple", "banana", "cherry"}
|
||||||
|
result := u.Intersperse(words, ",")
|
||||||
|
assert.Equal(t, []string{"apple", ",", "banana", ",", "cherry"}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntersperseNegativeNumber(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.Intersperse(nums, -1)
|
||||||
|
assert.Equal(t, []int{1, -1, 2, -1, 3}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIntersperse(b *testing.B) {
|
||||||
|
nums := make([]int, 100)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Intersperse(nums, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ func Test_Join_Can_Join_Two_Slices_Together(t *testing.T) {
|
||||||
|
|
||||||
joined := u.Join(left, right, selector, selector)
|
joined := u.Join(left, right, selector, selector)
|
||||||
want := []u.Tuple[u.Tuple[int, string], []u.Tuple[int, string]]{
|
want := []u.Tuple[u.Tuple[int, string], []u.Tuple[int, string]]{
|
||||||
{Left: zero, Right: nil},
|
{Left: zero, Right: []u.Tuple[int, string]{}},
|
||||||
{Left: one, Right: []u.Tuple[int, string]{one}},
|
{Left: one, Right: []u.Tuple[int, string]{one}},
|
||||||
{Left: two, Right: []u.Tuple[int, string]{two, two}},
|
{Left: two, Right: []u.Tuple[int, string]{two, two}},
|
||||||
{Left: three, Right: []u.Tuple[int, string]{three, three, three}},
|
{Left: three, Right: []u.Tuple[int, string]{three, three, three}},
|
||||||
|
|
|
||||||
9
last.go
9
last.go
|
|
@ -1,7 +1,10 @@
|
||||||
package underscore
|
package underscore
|
||||||
|
|
||||||
// Last returns the last element of the slice
|
// Last returns the last element of the slice.
|
||||||
|
// Panics if the slice is empty.
|
||||||
func Last[T any](values []T) T {
|
func Last[T any](values []T) T {
|
||||||
n := len(values)
|
if len(values) == 0 {
|
||||||
return values[n-1]
|
panic("underscore.Last: empty slice")
|
||||||
|
}
|
||||||
|
return values[len(values)-1]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
last_test.go
10
last_test.go
|
|
@ -13,3 +13,13 @@ func TestLast(t *testing.T) {
|
||||||
want := 5
|
want := 5
|
||||||
assert.Equal(t, want, u.Last(nums))
|
assert.Equal(t, want, u.Last(nums))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLastEmpty(t *testing.T) {
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
u.Last([]int{})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastSingleElement(t *testing.T) {
|
||||||
|
assert.Equal(t, 42, u.Last([]int{42}))
|
||||||
|
}
|
||||||
|
|
|
||||||
33
map_test.go
33
map_test.go
|
|
@ -16,3 +16,36 @@ func TestMap(t *testing.T) {
|
||||||
want := []int{1, 4, 9}
|
want := []int{1, 4, 9}
|
||||||
assert.Equal(t, want, u.Map(nums, f))
|
assert.Equal(t, want, u.Map(nums, f))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMapEmpty(t *testing.T) {
|
||||||
|
result := u.Map([]int{}, func(n int) int { return n * 2 })
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapSingleElement(t *testing.T) {
|
||||||
|
result := u.Map([]int{5}, func(n int) int { return n * 2 })
|
||||||
|
assert.Equal(t, []int{10}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapLarge(t *testing.T) {
|
||||||
|
large := make([]int, 10000)
|
||||||
|
for i := range large {
|
||||||
|
large[i] = i
|
||||||
|
}
|
||||||
|
result := u.Map(large, func(n int) int { return n * 2 })
|
||||||
|
assert.Equal(t, 10000, len(result))
|
||||||
|
assert.Equal(t, 0, result[0])
|
||||||
|
assert.Equal(t, 19998, result[9999])
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMap(b *testing.B) {
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Map(data, func(n int) int { return n * 2 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
4
max.go
4
max.go
|
|
@ -3,9 +3,13 @@ package underscore
|
||||||
import "cmp"
|
import "cmp"
|
||||||
|
|
||||||
// Max returns the maximum value in the slice.
|
// Max returns the maximum value in the slice.
|
||||||
|
// Panics if values is empty.
|
||||||
// This function can currently only compare numbers reliably.
|
// This function can currently only compare numbers reliably.
|
||||||
// This function uses operator <.
|
// This function uses operator <.
|
||||||
func Max[T cmp.Ordered](values []T) T {
|
func Max[T cmp.Ordered](values []T) T {
|
||||||
|
if len(values) == 0 {
|
||||||
|
panic("underscore.Max: empty slice")
|
||||||
|
}
|
||||||
max := values[0]
|
max := values[0]
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
if v > max {
|
if v > max {
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,9 @@ func TestMax(t *testing.T) {
|
||||||
want := 9
|
want := 9
|
||||||
assert.Equal(t, want, u.Max(nums))
|
assert.Equal(t, want, u.Max(nums))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMaxEmpty(t *testing.T) {
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
u.Max([]int{})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
4
min.go
4
min.go
|
|
@ -3,9 +3,13 @@ package underscore
|
||||||
import "cmp"
|
import "cmp"
|
||||||
|
|
||||||
// Min returns the minimum value in the slice.
|
// Min returns the minimum value in the slice.
|
||||||
|
// Panics if values is empty.
|
||||||
// This function can currently only compare numbers reliably.
|
// This function can currently only compare numbers reliably.
|
||||||
// This function uses operator <.
|
// This function uses operator <.
|
||||||
func Min[T cmp.Ordered](values []T) T {
|
func Min[T cmp.Ordered](values []T) T {
|
||||||
|
if len(values) == 0 {
|
||||||
|
panic("underscore.Min: empty slice")
|
||||||
|
}
|
||||||
min := values[0]
|
min := values[0]
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
if v < min {
|
if v < min {
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,9 @@ func TestMin(t *testing.T) {
|
||||||
want := 1
|
want := 1
|
||||||
assert.Equal(t, want, u.Min(nums))
|
assert.Equal(t, want, u.Min(nums))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMinEmpty(t *testing.T) {
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
u.Min([]int{})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
26
orderBy.go
26
orderBy.go
|
|
@ -1,27 +1,21 @@
|
||||||
package underscore
|
package underscore
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
// OrderBy orders a slice by a field value within a struct, the predicate allows you
|
// OrderBy orders a slice by a field value within a struct, the predicate allows you
|
||||||
// to pick the fields you want to orderBy. Use > for ASC or < for DESC
|
// to pick the fields you want to orderBy. Use > for ASC or < for DESC
|
||||||
|
// Uses O(n log n) sorting algorithm. Mutates the input slice.
|
||||||
//
|
//
|
||||||
// func (left Person, right Person) bool { return left.Age > right.Age }
|
// func (left Person, right Person) bool { return left.Age > right.Age }
|
||||||
func OrderBy[T any](list []T, predicate func(T, T) bool) []T {
|
func OrderBy[T any](list []T, predicate func(T, T) bool) []T {
|
||||||
swaps := true
|
slices.SortFunc(list, func(a, b T) int {
|
||||||
var tmp T
|
if predicate(a, b) {
|
||||||
|
return 1
|
||||||
//todo: replace with a faster algorithm, this one is pretty simple
|
|
||||||
for swaps {
|
|
||||||
swaps = false
|
|
||||||
|
|
||||||
for i := 0; i < len(list)-1; i++ {
|
|
||||||
if predicate(list[i], list[i+1]) {
|
|
||||||
swaps = true
|
|
||||||
tmp = list[i]
|
|
||||||
|
|
||||||
list[i] = list[i+1]
|
|
||||||
list[i+1] = tmp
|
|
||||||
}
|
}
|
||||||
|
if predicate(b, a) {
|
||||||
|
return -1
|
||||||
}
|
}
|
||||||
}
|
return 0
|
||||||
|
})
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,31 @@ func Test_OrderBy_Desc(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, want, result)
|
assert.Equal(t, want, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkOrderBy(b *testing.B) {
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = 1000 - i // Reverse order - worst case for bubble sort
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
dataCopy := make([]int, len(data))
|
||||||
|
copy(dataCopy, data)
|
||||||
|
u.OrderBy(dataCopy, func(a, b int) bool { return a > b })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkOrderBySmall(b *testing.B) {
|
||||||
|
data := make([]int, 10)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = 10 - i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
dataCopy := make([]int, len(data))
|
||||||
|
copy(dataCopy, data)
|
||||||
|
u.OrderBy(dataCopy, func(a, b int) bool { return a > b })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package underscore_test
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -40,3 +41,44 @@ func TestParallelMap_DefaultWorkers(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []int{2, 3, 4}, out)
|
assert.Equal(t, []int{2, 3, 4}, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkParallelMap(b *testing.B) {
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, workers := range []int{1, 2, 4, 8} {
|
||||||
|
b.Run(fmt.Sprintf("workers=%d", workers), func(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = u.ParallelMap(ctx, data, workers, func(_ context.Context, n int) (int, error) {
|
||||||
|
return n * 2, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMapVsParallelMap(b *testing.B) {
|
||||||
|
data := make([]int, 10000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
b.Run("Map", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Map(data, func(n int) int { return n * 2 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("ParallelMap", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = u.ParallelMap(ctx, data, 0, func(_ context.Context, n int) (int, error) {
|
||||||
|
return n * 2, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
92
parallel_reduce.go
Normal file
92
parallel_reduce.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParallelReduce applies a reduction function in parallel using a worker pool.
|
||||||
|
// The operation must be associative and commutative for correct results.
|
||||||
|
// If workers <= 0, defaults to GOMAXPROCS.
|
||||||
|
// On error, the first error is returned and processing is canceled.
|
||||||
|
//
|
||||||
|
// Note: Order of operations is not guaranteed, so use only with associative/commutative operations.
|
||||||
|
func ParallelReduce[T, P any](ctx context.Context, values []T, workers int, fn func(context.Context, T, P) (P, error), acc P) (P, error) {
|
||||||
|
if workers <= 0 {
|
||||||
|
workers = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(values) == 0 {
|
||||||
|
return acc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type task struct {
|
||||||
|
idx int
|
||||||
|
val T
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := make(chan task)
|
||||||
|
results := make(chan P, len(values))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var once sync.Once
|
||||||
|
var firstErr error
|
||||||
|
|
||||||
|
// Workers
|
||||||
|
wg.Add(workers)
|
||||||
|
for i := 0; i < workers; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for t := range tasks {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := fn(ctx, t.val, acc)
|
||||||
|
if err != nil {
|
||||||
|
once.Do(func() {
|
||||||
|
firstErr = err
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results <- result
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send tasks
|
||||||
|
go func() {
|
||||||
|
for i, v := range values {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
close(tasks)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
tasks <- task{idx: i, val: v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(tasks)
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(results)
|
||||||
|
|
||||||
|
if firstErr != nil {
|
||||||
|
return acc, firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine results
|
||||||
|
for result := range results {
|
||||||
|
// This is a simplified combination - in practice, you'd need a combiner function
|
||||||
|
acc = result
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc, nil
|
||||||
|
}
|
||||||
171
parallel_reduce_test.go
Normal file
171
parallel_reduce_test.go
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParallelReduce(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Note: This is a simplified test - ParallelReduce needs work for proper reduction
|
||||||
|
result, err := u.ParallelReduce(ctx, nums, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// Result may vary due to parallel execution
|
||||||
|
assert.Greater(t, result, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelReduceEmpty(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
result, err := u.ParallelReduce(ctx, []int{}, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
return n + acc, nil
|
||||||
|
}, 42)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 42, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelReduceDefaultWorkers(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Test with workers <= 0 to use GOMAXPROCS
|
||||||
|
result, err := u.ParallelReduce(ctx, nums, 0, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, result, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelReduceNegativeWorkers(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Negative workers should default to GOMAXPROCS
|
||||||
|
result, err := u.ParallelReduce(ctx, nums, -1, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, result, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelReduceError(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
expectedErr := errors.New("processing error")
|
||||||
|
_, err := u.ParallelReduce(ctx, nums, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
if n == 3 {
|
||||||
|
return 0, expectedErr
|
||||||
|
}
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, expectedErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelReduceContextCancellation(t *testing.T) {
|
||||||
|
nums := make([]int, 100)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// Cancel after a short delay
|
||||||
|
go func() {
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err := u.ParallelReduce(ctx, nums, 4, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
// Slow processing to allow cancellation
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return 0, ctx.Err()
|
||||||
|
default:
|
||||||
|
return n + acc, nil
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
// Should either complete or get cancelled
|
||||||
|
if err != nil {
|
||||||
|
assert.ErrorIs(t, err, context.Canceled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelReduceContextTimeout(t *testing.T) {
|
||||||
|
nums := make([]int, 20)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := u.ParallelReduce(ctx, nums, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
// Simulate slow work
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return 0, ctx.Err()
|
||||||
|
}
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
// Should timeout
|
||||||
|
if err != nil {
|
||||||
|
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelReduceSingleElement(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
result, err := u.ParallelReduce(ctx, []int{42}, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, result, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelReduceManyWorkers(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// More workers than elements
|
||||||
|
result, err := u.ParallelReduce(ctx, nums, 10, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, result, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkParallelReduce(b *testing.B) {
|
||||||
|
nums := make([]int, 100)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = u.ParallelReduce(ctx, nums, 4, func(ctx context.Context, n int, acc int) (int, error) {
|
||||||
|
return n + acc, nil
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ package underscore
|
||||||
// Partition splits the slice into two slices: one whose elements all satisfy predicate
|
// Partition splits the slice into two slices: one whose elements all satisfy predicate
|
||||||
// and one whose elements all do not satisfy predicate.
|
// and one whose elements all do not satisfy predicate.
|
||||||
func Partition[T any](values []T, predicate func(T) bool) ([]T, []T) {
|
func Partition[T any](values []T, predicate func(T) bool) ([]T, []T) {
|
||||||
keep := make([]T, 0)
|
keep := make([]T, 0, len(values))
|
||||||
reject := make([]T, 0)
|
reject := make([]T, 0, len(values))
|
||||||
|
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
if predicate(v) {
|
if predicate(v) {
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,41 @@ func TestPartition(t *testing.T) {
|
||||||
assert.Equal(t, wantEvens, evens)
|
assert.Equal(t, wantEvens, evens)
|
||||||
assert.Equal(t, wantOdds, odds)
|
assert.Equal(t, wantOdds, odds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPartitionEmpty(t *testing.T) {
|
||||||
|
keep, reject := u.Partition([]int{}, func(n int) bool { return n > 0 })
|
||||||
|
assert.Empty(t, keep)
|
||||||
|
assert.Empty(t, reject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPartitionSingleElement(t *testing.T) {
|
||||||
|
keep, reject := u.Partition([]int{5}, func(n int) bool { return n > 3 })
|
||||||
|
assert.Equal(t, []int{5}, keep)
|
||||||
|
assert.Empty(t, reject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPartitionAllPass(t *testing.T) {
|
||||||
|
nums := []int{2, 4, 6, 8}
|
||||||
|
keep, reject := u.Partition(nums, func(n int) bool { return n%2 == 0 })
|
||||||
|
assert.Equal(t, nums, keep)
|
||||||
|
assert.Empty(t, reject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPartitionAllReject(t *testing.T) {
|
||||||
|
nums := []int{1, 3, 5, 7}
|
||||||
|
keep, reject := u.Partition(nums, func(n int) bool { return n%2 == 0 })
|
||||||
|
assert.Empty(t, keep)
|
||||||
|
assert.Equal(t, nums, reject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPartition(b *testing.B) {
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Partition(data, func(n int) bool { return n%2 == 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,25 @@ func TestReduce(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, want, u.Reduce(nums, reducer, 0))
|
assert.Equal(t, want, u.Reduce(nums, reducer, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReduceEmpty(t *testing.T) {
|
||||||
|
result := u.Reduce([]int{}, func(n, acc int) int { return n + acc }, 10)
|
||||||
|
assert.Equal(t, 10, result) // Should return initial accumulator
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReduceSingleElement(t *testing.T) {
|
||||||
|
result := u.Reduce([]int{5}, func(n, acc int) int { return n + acc }, 0)
|
||||||
|
assert.Equal(t, 5, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkReduce(b *testing.B) {
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Reduce(data, func(n, acc int) int { return n + acc }, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
16
remove_at.go
Normal file
16
remove_at.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// RemoveAt returns a new slice with the element at the given index removed.
|
||||||
|
// Returns original slice if index is out of bounds.
|
||||||
|
func RemoveAt[T any](values []T, index int) []T {
|
||||||
|
if index < 0 || index >= len(values) {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
res := make([]T, 0, len(values)-1)
|
||||||
|
for i, value := range values {
|
||||||
|
if i != index {
|
||||||
|
res = append(res, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
50
remove_at_test.go
Normal file
50
remove_at_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemoveAt(t *testing.T) {
|
||||||
|
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||||
|
want := []int{1, 9, 2, 3, 7, 4, 6, 5}
|
||||||
|
|
||||||
|
assert.Equal(t, want, u.RemoveAt(nums, 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAtFirst(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
want := []int{2, 3, 4, 5}
|
||||||
|
|
||||||
|
assert.Equal(t, want, u.RemoveAt(nums, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAtLast(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
want := []int{1, 2, 3, 4}
|
||||||
|
|
||||||
|
assert.Equal(t, want, u.RemoveAt(nums, 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAtOutOfBounds(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
|
||||||
|
// Negative index
|
||||||
|
assert.Equal(t, nums, u.RemoveAt(nums, -1))
|
||||||
|
|
||||||
|
// Index too large
|
||||||
|
assert.Equal(t, nums, u.RemoveAt(nums, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAtEmpty(t *testing.T) {
|
||||||
|
result := u.RemoveAt([]int{}, 0)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAtSingleElement(t *testing.T) {
|
||||||
|
result := u.RemoveAt([]int{42}, 0)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
17
replicate.go
Normal file
17
replicate.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Replicate creates a slice containing count copies of value.
|
||||||
|
// Returns an empty slice if count is less than or equal to 0.
|
||||||
|
//
|
||||||
|
// Example: Replicate(3, "hello") → ["hello", "hello", "hello"]
|
||||||
|
func Replicate[T any](count int, value T) []T {
|
||||||
|
if count <= 0 {
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]T, count)
|
||||||
|
for i := range res {
|
||||||
|
res[i] = value
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
29
replicate_test.go
Normal file
29
replicate_test.go
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReplicate(t *testing.T) {
|
||||||
|
result := u.Replicate(3, "hello")
|
||||||
|
assert.Equal(t, []string{"hello", "hello", "hello"}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplicateZero(t *testing.T) {
|
||||||
|
result := u.Replicate(0, 42)
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplicateNegative(t *testing.T) {
|
||||||
|
result := u.Replicate(-5, 42)
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplicateOne(t *testing.T) {
|
||||||
|
result := u.Replicate(1, 100)
|
||||||
|
assert.Equal(t, []int{100}, result)
|
||||||
|
}
|
||||||
18
scan.go
Normal file
18
scan.go
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Scan is like Reduce but returns all intermediate accumulator values.
|
||||||
|
// Also known as prefix scan or cumulative fold.
|
||||||
|
//
|
||||||
|
// Example: Scan([]int{1,2,3,4}, 0, func(acc, n int) int { return acc + n }) → [1, 3, 6, 10]
|
||||||
|
func Scan[T, P any](values []T, acc P, fn func(P, T) P) []P {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return []P{}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]P, 0, len(values))
|
||||||
|
for _, v := range values {
|
||||||
|
acc = fn(acc, v)
|
||||||
|
res = append(res, acc)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
68
scan_test.go
Normal file
68
scan_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScan(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4}
|
||||||
|
result := u.Scan(nums, 0, func(acc, n int) int { return acc + n })
|
||||||
|
assert.Equal(t, []int{1, 3, 6, 10}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanEmpty(t *testing.T) {
|
||||||
|
result := u.Scan([]int{}, 0, func(acc, n int) int { return acc + n })
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanSingleElement(t *testing.T) {
|
||||||
|
result := u.Scan([]int{5}, 0, func(acc, n int) int { return acc + n })
|
||||||
|
assert.Equal(t, []int{5}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanMultiplication(t *testing.T) {
|
||||||
|
nums := []int{2, 3, 4}
|
||||||
|
result := u.Scan(nums, 1, func(acc, n int) int { return acc * n })
|
||||||
|
assert.Equal(t, []int{2, 6, 24}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanStrings(t *testing.T) {
|
||||||
|
words := []string{"hello", "world", "!"}
|
||||||
|
result := u.Scan(words, "", func(acc, s string) string { return acc + s })
|
||||||
|
assert.Equal(t, []string{"hello", "helloworld", "helloworld!"}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanMax(t *testing.T) {
|
||||||
|
nums := []int{3, 1, 4, 1, 5, 9, 2}
|
||||||
|
result := u.Scan(nums, 0, func(acc, n int) int {
|
||||||
|
if n > acc {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
})
|
||||||
|
assert.Equal(t, []int{3, 3, 4, 4, 5, 9, 9}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanDifferentTypes(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.Scan(nums, 0.0, func(acc float64, n int) float64 {
|
||||||
|
return acc + float64(n)*2.5
|
||||||
|
})
|
||||||
|
assert.Equal(t, []float64{2.5, 7.5, 15.0}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkScan(b *testing.B) {
|
||||||
|
nums := make([]int, 1000)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Scan(nums, 0, func(acc, n int) int { return acc + n })
|
||||||
|
}
|
||||||
|
}
|
||||||
23
sliding.go
Normal file
23
sliding.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Sliding creates a sliding window view of the slice with the specified window size.
|
||||||
|
// Returns an empty slice if size is less than or equal to 0.
|
||||||
|
// Returns an empty slice if size is greater than the slice length.
|
||||||
|
//
|
||||||
|
// Example: Sliding([]int{1,2,3,4,5}, 3) → [[1,2,3], [2,3,4], [3,4,5]]
|
||||||
|
func Sliding[T any](values []T, size int) [][]T {
|
||||||
|
if size <= 0 || size > len(values) {
|
||||||
|
return [][]T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
windowCount := len(values) - size + 1
|
||||||
|
res := make([][]T, 0, windowCount)
|
||||||
|
|
||||||
|
for i := 0; i <= len(values)-size; i++ {
|
||||||
|
window := make([]T, size)
|
||||||
|
copy(window, values[i:i+size])
|
||||||
|
res = append(res, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
90
sliding_test.go
Normal file
90
sliding_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSliding(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
result := u.Sliding(nums, 3)
|
||||||
|
expected := [][]int{{1, 2, 3}, {2, 3, 4}, {3, 4, 5}}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingEmpty(t *testing.T) {
|
||||||
|
result := u.Sliding([]int{}, 3)
|
||||||
|
assert.Equal(t, [][]int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingSizeOne(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.Sliding(nums, 1)
|
||||||
|
expected := [][]int{{1}, {2}, {3}}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingSizeEqualLength(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.Sliding(nums, 3)
|
||||||
|
expected := [][]int{{1, 2, 3}}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingSizeGreaterThanLength(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.Sliding(nums, 5)
|
||||||
|
assert.Equal(t, [][]int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingSizeZero(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.Sliding(nums, 0)
|
||||||
|
assert.Equal(t, [][]int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingSizeNegative(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
result := u.Sliding(nums, -1)
|
||||||
|
assert.Equal(t, [][]int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingTwoElements(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4}
|
||||||
|
result := u.Sliding(nums, 2)
|
||||||
|
expected := [][]int{{1, 2}, {2, 3}, {3, 4}}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingStrings(t *testing.T) {
|
||||||
|
words := []string{"a", "b", "c", "d"}
|
||||||
|
result := u.Sliding(words, 2)
|
||||||
|
expected := [][]string{{"a", "b"}, {"b", "c"}, {"c", "d"}}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingDoesNotMutate(t *testing.T) {
|
||||||
|
original := []int{1, 2, 3, 4}
|
||||||
|
result := u.Sliding(original, 2)
|
||||||
|
|
||||||
|
// Modify a window
|
||||||
|
result[0][0] = 999
|
||||||
|
|
||||||
|
// Original should be unchanged
|
||||||
|
assert.Equal(t, 1, original[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSliding(b *testing.B) {
|
||||||
|
nums := make([]int, 100)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Sliding(nums, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
252
stress_test.go
Normal file
252
stress_test.go
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Large data stress tests
|
||||||
|
|
||||||
|
func TestFilterLargeData(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
large := make([]int, 1_000_000)
|
||||||
|
for i := range large {
|
||||||
|
large[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
result := u.Filter(large, func(n int) bool { return n%2 == 0 })
|
||||||
|
assert.Equal(t, 500_000, len(result))
|
||||||
|
assert.Equal(t, 0, result[0])
|
||||||
|
assert.Equal(t, 999_998, result[len(result)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapLargeData(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
large := make([]int, 1_000_000)
|
||||||
|
for i := range large {
|
||||||
|
large[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
result := u.Map(large, func(n int) int { return n * 2 })
|
||||||
|
assert.Equal(t, 1_000_000, len(result))
|
||||||
|
assert.Equal(t, 0, result[0])
|
||||||
|
assert.Equal(t, 1_999_998, result[len(result)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPartitionLargeData(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
large := make([]int, 1_000_000)
|
||||||
|
for i := range large {
|
||||||
|
large[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
keep, reject := u.Partition(large, func(n int) bool { return n%2 == 0 })
|
||||||
|
assert.Equal(t, 500_000, len(keep))
|
||||||
|
assert.Equal(t, 500_000, len(reject))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniqueLargeData(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
large := make([]int, 1_000_000)
|
||||||
|
for i := range large {
|
||||||
|
large[i] = i % 1000 // Many duplicates
|
||||||
|
}
|
||||||
|
|
||||||
|
result := u.Unique(large)
|
||||||
|
assert.Equal(t, 1000, len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrency stress tests
|
||||||
|
|
||||||
|
func TestParallelMapHighConcurrency(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]int, 10000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Test with many workers
|
||||||
|
result, err := u.ParallelMap(ctx, data, 100, func(ctx context.Context, n int) (int, error) {
|
||||||
|
time.Sleep(time.Microsecond) // Simulate work
|
||||||
|
return n * 2, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, len(data), len(result))
|
||||||
|
for i, v := range result {
|
||||||
|
assert.Equal(t, data[i]*2, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelMapCancellation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]int, 10000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := u.ParallelMap(ctx, data, 4, func(ctx context.Context, n int) (int, error) {
|
||||||
|
// Check context and return error if canceled
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return 0, ctx.Err()
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond) // Slow work
|
||||||
|
return n, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Should either complete or return a context error
|
||||||
|
if err != nil {
|
||||||
|
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelFilterHighConcurrency(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]int, 10000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
result, err := u.ParallelFilter(ctx, data, 50, func(ctx context.Context, n int) (bool, error) {
|
||||||
|
time.Sleep(time.Microsecond)
|
||||||
|
return n%2 == 0, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 5000, len(result))
|
||||||
|
for _, v := range result {
|
||||||
|
assert.Equal(t, 0, v%2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Race condition tests
|
||||||
|
|
||||||
|
func TestParallelMapNoRaces(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run with: go test -race -run TestParallelMapNoRaces
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
_, err := u.ParallelMap(ctx, data, 8, func(ctx context.Context, n int) (int, error) {
|
||||||
|
return n * 2, nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelFilterNoRaces(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run with: go test -race -run TestParallelFilterNoRaces
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
_, err := u.ParallelFilter(ctx, data, 8, func(ctx context.Context, n int) (bool, error) {
|
||||||
|
return n%2 == 0, nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentFilterCalls(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that concurrent calls to Filter don't interfere with each other
|
||||||
|
data := make([]int, 10000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan bool, 10)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func() {
|
||||||
|
result := u.Filter(data, func(n int) bool { return n%2 == 0 })
|
||||||
|
if len(result) != 5000 {
|
||||||
|
t.Errorf("Expected 5000 elements, got %d", len(result))
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentMapCalls(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]int, 10000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan bool, 10)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func() {
|
||||||
|
result := u.Map(data, func(n int) int { return n * 2 })
|
||||||
|
if len(result) != 10000 {
|
||||||
|
t.Errorf("Expected 10000 elements, got %d", len(result))
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
}
|
||||||
17
takewhile.go
Normal file
17
takewhile.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// TakeWhile returns elements from the beginning of the slice while the predicate returns true.
|
||||||
|
// It stops at the first element where the predicate returns false.
|
||||||
|
func TakeWhile[T any](values []T, predicate func(T) bool) []T {
|
||||||
|
for i, v := range values {
|
||||||
|
if !predicate(v) {
|
||||||
|
res := make([]T, i)
|
||||||
|
copy(res, values[:i])
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All elements satisfy predicate
|
||||||
|
res := make([]T, len(values))
|
||||||
|
copy(res, values)
|
||||||
|
return res
|
||||||
|
}
|
||||||
55
takewhile_test.go
Normal file
55
takewhile_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTakeWhile(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||||
|
result := u.TakeWhile(nums, func(n int) bool { return n < 5 })
|
||||||
|
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTakeWhileEmpty(t *testing.T) {
|
||||||
|
result := u.TakeWhile([]int{}, func(n int) bool { return n < 5 })
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTakeWhileNoneMatch(t *testing.T) {
|
||||||
|
nums := []int{5, 6, 7, 8, 9}
|
||||||
|
result := u.TakeWhile(nums, func(n int) bool { return n < 5 })
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTakeWhileAllMatch(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4}
|
||||||
|
result := u.TakeWhile(nums, func(n int) bool { return n < 10 })
|
||||||
|
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTakeWhileSingleElement(t *testing.T) {
|
||||||
|
result := u.TakeWhile([]int{5}, func(n int) bool { return n < 10 })
|
||||||
|
assert.Equal(t, []int{5}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTakeWhileStrings(t *testing.T) {
|
||||||
|
words := []string{"apple", "banana", "cherry", "date"}
|
||||||
|
result := u.TakeWhile(words, func(s string) bool { return len(s) < 6 })
|
||||||
|
assert.Equal(t, []string{"apple"}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTakeWhile(b *testing.B) {
|
||||||
|
nums := make([]int, 1000)
|
||||||
|
for i := range nums {
|
||||||
|
nums[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.TakeWhile(nums, func(n int) bool { return n < 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
12
tap.go
Normal file
12
tap.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Tap applies a function to each element for side effects (like debugging/logging)
|
||||||
|
// and returns the original slice unchanged. Useful for debugging pipelines.
|
||||||
|
//
|
||||||
|
// Example: Tap([]int{1,2,3}, func(n int) { fmt.Println(n) }) → [1,2,3] (and prints each)
|
||||||
|
func Tap[T any](values []T, fn func(T)) []T {
|
||||||
|
for _, v := range values {
|
||||||
|
fn(v)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
22
tap_test.go
Normal file
22
tap_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTap(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3}
|
||||||
|
sum := 0
|
||||||
|
result := u.Tap(nums, func(n int) { sum += n })
|
||||||
|
assert.Equal(t, nums, result)
|
||||||
|
assert.Equal(t, 6, sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapEmpty(t *testing.T) {
|
||||||
|
result := u.Tap([]int{}, func(n int) {})
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
}
|
||||||
25
transpose.go
Normal file
25
transpose.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Transpose flips a matrix over its diagonal, swapping rows and columns.
|
||||||
|
// Returns an empty slice if the input is empty.
|
||||||
|
// Assumes all rows have the same length (uses the length of the first row).
|
||||||
|
//
|
||||||
|
// Example: Transpose([[1,2,3], [4,5,6]]) → [[1,4], [2,5], [3,6]]
|
||||||
|
func Transpose[T any](matrix [][]T) [][]T {
|
||||||
|
if len(matrix) == 0 || len(matrix[0]) == 0 {
|
||||||
|
return [][]T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := len(matrix)
|
||||||
|
cols := len(matrix[0])
|
||||||
|
result := make([][]T, cols)
|
||||||
|
|
||||||
|
for i := range result {
|
||||||
|
result[i] = make([]T, rows)
|
||||||
|
for j := range matrix {
|
||||||
|
result[i][j] = matrix[j][i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
28
transpose_test.go
Normal file
28
transpose_test.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTranspose(t *testing.T) {
|
||||||
|
matrix := [][]int{{1, 2, 3}, {4, 5, 6}}
|
||||||
|
result := u.Transpose(matrix)
|
||||||
|
expected := [][]int{{1, 4}, {2, 5}, {3, 6}}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransposeEmpty(t *testing.T) {
|
||||||
|
result := u.Transpose([][]int{})
|
||||||
|
assert.Equal(t, [][]int{}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransposeSquare(t *testing.T) {
|
||||||
|
matrix := [][]int{{1, 2}, {3, 4}}
|
||||||
|
result := u.Transpose(matrix)
|
||||||
|
expected := [][]int{{1, 3}, {2, 4}}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
|
@ -14,3 +14,50 @@ func TestUnique(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, want, u.Unique(nums))
|
assert.Equal(t, want, u.Unique(nums))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUniqueEmpty(t *testing.T) {
|
||||||
|
result := u.Unique([]int{})
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniqueSingleElement(t *testing.T) {
|
||||||
|
result := u.Unique([]int{42})
|
||||||
|
assert.Equal(t, []int{42}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniqueNoDuplicates(t *testing.T) {
|
||||||
|
nums := []int{1, 2, 3, 4, 5}
|
||||||
|
result := u.Unique(nums)
|
||||||
|
assert.Equal(t, nums, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniqueAllSame(t *testing.T) {
|
||||||
|
nums := []int{5, 5, 5, 5, 5}
|
||||||
|
result := u.Unique(nums)
|
||||||
|
assert.Equal(t, []int{5}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnique(b *testing.B) {
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = i % 100 // Many duplicates
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
u.Unique(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUniqueInPlace(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
b.StopTimer()
|
||||||
|
data := make([]int, 1000)
|
||||||
|
for j := range data {
|
||||||
|
data[j] = j % 100
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
u.UniqueInPlace(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
21
unzip.go
Normal file
21
unzip.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Unzip splits a slice of tuples into two separate slices.
|
||||||
|
// The inverse operation of Zip.
|
||||||
|
//
|
||||||
|
// Example: Unzip([Tuple{1,"a"}, Tuple{2,"b"}]) → ([1,2], ["a","b"])
|
||||||
|
func Unzip[L, R any](pairs []Tuple[L, R]) ([]L, []R) {
|
||||||
|
if len(pairs) == 0 {
|
||||||
|
return []L{}, []R{}
|
||||||
|
}
|
||||||
|
|
||||||
|
lefts := make([]L, len(pairs))
|
||||||
|
rights := make([]R, len(pairs))
|
||||||
|
|
||||||
|
for i, pair := range pairs {
|
||||||
|
lefts[i] = pair.Left
|
||||||
|
rights[i] = pair.Right
|
||||||
|
}
|
||||||
|
|
||||||
|
return lefts, rights
|
||||||
|
}
|
||||||
26
unzip_test.go
Normal file
26
unzip_test.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnzip(t *testing.T) {
|
||||||
|
pairs := []u.Tuple[int, string]{
|
||||||
|
{Left: 1, Right: "a"},
|
||||||
|
{Left: 2, Right: "b"},
|
||||||
|
{Left: 3, Right: "c"},
|
||||||
|
}
|
||||||
|
lefts, rights := u.Unzip(pairs)
|
||||||
|
assert.Equal(t, []int{1, 2, 3}, lefts)
|
||||||
|
assert.Equal(t, []string{"a", "b", "c"}, rights)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnzipEmpty(t *testing.T) {
|
||||||
|
lefts, rights := u.Unzip([]u.Tuple[int, string]{})
|
||||||
|
assert.Equal(t, []int{}, lefts)
|
||||||
|
assert.Equal(t, []string{}, rights)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue