mirror of
https://github.com/rjNemo/underscore
synced 2026-06-12 13:36:40 +00:00
Compare commits
No commits in common. "d622c8cba878068641c520d76391a6c1aaa0a0d5" and "c53d46816f9e37ba40d730a95024e603f97e1f6c" have entirely different histories.
d622c8cba8
...
c53d46816f
29 changed files with 43 additions and 2064 deletions
1189
ACTION_PLAN.md
1189
ACTION_PLAN.md
File diff suppressed because it is too large
Load diff
183
CLAUDE.md
183
CLAUDE.md
|
|
@ -1,183 +0,0 @@
|
||||||
# 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@latest
|
go get github.com/rjNemo/underscore@0.7.0
|
||||||
```
|
```
|
||||||
|
|
||||||
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,33 +92,22 @@ make test
|
||||||
|
|
||||||
- `All`
|
- `All`
|
||||||
- `Any`
|
- `Any`
|
||||||
- `Chunk`
|
|
||||||
- `Contains`
|
- `Contains`
|
||||||
- `ContainsBy`
|
- `ContainsBy`
|
||||||
- `Count`
|
|
||||||
- `Difference`
|
|
||||||
- `Drop`
|
|
||||||
- `Each`
|
- `Each`
|
||||||
- `Filter`
|
- `Filter`
|
||||||
- `Find`
|
|
||||||
- `Flatmap`
|
- `Flatmap`
|
||||||
- `GroupBy`
|
- `GroupBy`
|
||||||
- `Intersection`
|
- `Find`
|
||||||
- `Join` / `JoinProject`
|
|
||||||
- `Last`
|
|
||||||
- `Map`
|
- `Map`
|
||||||
- `Max`
|
- `Max`
|
||||||
- `Min`
|
- `Min`
|
||||||
- `OrderBy`
|
|
||||||
- `Partition`
|
- `Partition`
|
||||||
- `Range`
|
|
||||||
- `Reduce`
|
- `Reduce`
|
||||||
- `RemoveAt`
|
|
||||||
- `Sum` / `SumMap`
|
|
||||||
- `Unique`
|
- `Unique`
|
||||||
- `UniqueBy`
|
- `UniqueBy`
|
||||||
- `UniqueInPlace`
|
- `UniqueInPlace`
|
||||||
- `Zip`
|
- `Chunk`
|
||||||
|
|
||||||
### Pipe
|
### Pipe
|
||||||
|
|
||||||
|
|
@ -168,18 +157,9 @@ 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)`: extract keys or values from maps
|
- `maps.Keys(m)` / `maps.Values(m)`: utilities to extract keys or values from maps.
|
||||||
- `maps.Map(m, fn)`: transform map entries
|
|
||||||
|
|
||||||
## Built With
|
## Built With
|
||||||
|
|
||||||
|
|
|
||||||
20
drop.go
20
drop.go
|
|
@ -1,16 +1,12 @@
|
||||||
package underscore
|
package underscore
|
||||||
|
|
||||||
// Drop returns a new slice with the first n elements removed.
|
// Drop returns the rest of the elements in a slice.
|
||||||
// If n is greater than or equal to the slice length, returns an empty slice.
|
// Pass an index to return the values of the slice from that index onward.
|
||||||
// If n is less than or equal to 0, returns the original slice.
|
func Drop[T any](values []T, index int) (rest []T) {
|
||||||
func Drop[T any](values []T, n int) []T {
|
for i, value := range values {
|
||||||
if n <= 0 {
|
if i != index {
|
||||||
return values
|
rest = append(rest, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if n >= len(values) {
|
return rest
|
||||||
return []T{}
|
|
||||||
}
|
|
||||||
res := make([]T, len(values)-n)
|
|
||||||
copy(res, values[n:])
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
drop_test.go
32
drop_test.go
|
|
@ -9,34 +9,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDrop(t *testing.T) {
|
func TestDrop(t *testing.T) {
|
||||||
nums := []int{1, 2, 3, 4, 5}
|
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||||
want := []int{3, 4, 5}
|
want := []int{1, 9, 2, 3, 7, 4, 6, 5}
|
||||||
|
|
||||||
assert.Equal(t, want, u.Drop(nums, 2))
|
assert.Equal(t, want, u.Drop(nums, 3))
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ 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,40 +15,3 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ 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 {
|
||||||
// Estimate capacity: assume average of 2-3 items per element
|
res := make([]T, 0)
|
||||||
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,16 +15,3 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
5
go.mod
5
go.mod
|
|
@ -2,7 +2,10 @@ module github.com/rjNemo/underscore
|
||||||
|
|
||||||
go 1.24.2
|
go 1.24.2
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.8.4
|
require (
|
||||||
|
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,6 +4,8 @@ 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, len(values)/10)
|
res := make(map[K][]V, 0)
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -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: []u.Tuple[int, string]{}},
|
{Left: zero, Right: nil},
|
||||||
{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}},
|
||||||
|
|
|
||||||
10
last_test.go
10
last_test.go
|
|
@ -13,13 +13,3 @@ 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,36 +16,3 @@ 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,13 +3,9 @@ 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,9 +13,3 @@ 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,13 +3,9 @@ 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,9 +13,3 @@ 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{})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
28
orderBy.go
28
orderBy.go
|
|
@ -1,21 +1,27 @@
|
||||||
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 {
|
||||||
slices.SortFunc(list, func(a, b T) int {
|
swaps := true
|
||||||
if predicate(a, b) {
|
var tmp T
|
||||||
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,31 +29,3 @@ 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,7 +3,6 @@ package underscore_test
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -41,44 +40,3 @@ 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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, len(values))
|
keep := make([]T, 0)
|
||||||
reject := make([]T, 0, len(values))
|
reject := make([]T, 0)
|
||||||
|
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
if predicate(v) {
|
if predicate(v) {
|
||||||
|
|
|
||||||
|
|
@ -20,41 +20,3 @@ 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,25 +17,3 @@ 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
16
remove_at.go
|
|
@ -1,16 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
252
stress_test.go
252
stress_test.go
|
|
@ -1,252 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,50 +14,3 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue