mirror of
https://github.com/rjNemo/underscore
synced 2026-06-12 13:36:40 +00:00
Compare commits
12 commits
c53d46816f
...
d622c8cba8
| Author | SHA1 | Date | |
|---|---|---|---|
| d622c8cba8 | |||
| 6576c4fea7 | |||
| 07d05425bb | |||
| 75eddcdde5 | |||
| b04e545d03 | |||
| 40ac16261e | |||
| 106b713cc5 | |||
| a1943556b4 | |||
| 46d52e3cfa | |||
| 7caa23e082 | |||
| 92b64630dc | |||
| 7580836815 |
29 changed files with 2064 additions and 43 deletions
1189
ACTION_PLAN.md
Normal file
1189
ACTION_PLAN.md
Normal file
File diff suppressed because it is too large
Load diff
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
|
||||||
|
|
||||||
|
|
|
||||||
20
drop.go
20
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
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return rest
|
if n >= len(values) {
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -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}},
|
||||||
|
|
|
||||||
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{})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
28
orderBy.go
28
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue