Compare commits

..

No commits in common. "d622c8cba878068641c520d76391a6c1aaa0a0d5" and "c53d46816f9e37ba40d730a95024e603f97e1f6c" have entirely different histories.

29 changed files with 43 additions and 2064 deletions

File diff suppressed because it is too large Load diff

183
CLAUDE.md
View file

@ -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

View file

@ -21,7 +21,7 @@ It is mostly a port from the `underscore.js` library based on generics brought b
Install the library using
```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.
@ -92,33 +92,22 @@ make test
- `All`
- `Any`
- `Chunk`
- `Contains`
- `ContainsBy`
- `Count`
- `Difference`
- `Drop`
- `Each`
- `Filter`
- `Find`
- `Flatmap`
- `GroupBy`
- `Intersection`
- `Join` / `JoinProject`
- `Last`
- `Find`
- `Map`
- `Max`
- `Min`
- `OrderBy`
- `Partition`
- `Range`
- `Reduce`
- `RemoveAt`
- `Sum` / `SumMap`
- `Unique`
- `UniqueBy`
- `UniqueInPlace`
- `Zip`
- `Chunk`
### 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
- `maps.Keys(m)` / `maps.Values(m)`: extract keys or values from maps
- `maps.Map(m, fn)`: transform map entries
- `maps.Keys(m)` / `maps.Values(m)`: utilities to extract keys or values from maps.
## Built With

20
drop.go
View file

@ -1,16 +1,12 @@
package underscore
// Drop returns a new slice with the first n elements removed.
// If n is greater than or equal to the slice length, returns an empty slice.
// If n is less than or equal to 0, returns the original slice.
func Drop[T any](values []T, n int) []T {
if n <= 0 {
return values
// Drop returns the rest of the elements in a slice.
// Pass an index to return the values of the slice from that index onward.
func Drop[T any](values []T, index int) (rest []T) {
for i, value := range values {
if i != index {
rest = append(rest, value)
}
}
if n >= len(values) {
return []T{}
}
res := make([]T, len(values)-n)
copy(res, values[n:])
return res
return rest
}

View file

@ -9,34 +9,8 @@ import (
)
func TestDrop(t *testing.T) {
nums := []int{1, 2, 3, 4, 5}
want := []int{3, 4, 5}
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.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))
assert.Equal(t, want, u.Drop(nums, 3))
}

View file

@ -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).
func Filter[T any](values []T, predicate func(T) bool) (res []T) {
res = make([]T, 0, len(values))
for _, v := range values {
if predicate(v) {
res = append(res, v)

View file

@ -15,40 +15,3 @@ func TestFilter(t *testing.T) {
want := []int{0, 2, 4, 6, 8}
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)
}
}

View file

@ -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.
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, len(values)*2)
res := make([]T, 0)
for _, v := range values {
vs := mapper(v)
res = append(res, vs...)

View file

@ -15,16 +15,3 @@ func TestFlatmap(t *testing.T) {
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
View file

@ -2,7 +2,10 @@ module github.com/rjNemo/underscore
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 (
github.com/davecgh/go-spew v1.1.1 // indirect

2
go.sum
View file

@ -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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -2,7 +2,7 @@ package underscore
// 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 {
res := make(map[K][]V, len(values)/10)
res := make(map[K][]V, 0)
for _, v := range values {
k := f(v)
if r, ok := res[k]; ok {

View file

@ -23,7 +23,7 @@ func Test_Join_Can_Join_Two_Slices_Together(t *testing.T) {
joined := u.Join(left, right, selector, selector)
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: two, Right: []u.Tuple[int, string]{two, two}},
{Left: three, Right: []u.Tuple[int, string]{three, three, three}},

View file

@ -13,13 +13,3 @@ func TestLast(t *testing.T) {
want := 5
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}))
}

View file

@ -16,36 +16,3 @@ func TestMap(t *testing.T) {
want := []int{1, 4, 9}
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
View file

@ -3,13 +3,9 @@ package underscore
import "cmp"
// Max returns the maximum value in the slice.
// Panics if values is empty.
// This function can currently only compare numbers reliably.
// This function uses operator <.
func Max[T cmp.Ordered](values []T) T {
if len(values) == 0 {
panic("underscore.Max: empty slice")
}
max := values[0]
for _, v := range values {
if v > max {

View file

@ -13,9 +13,3 @@ func TestMax(t *testing.T) {
want := 9
assert.Equal(t, want, u.Max(nums))
}
func TestMaxEmpty(t *testing.T) {
assert.Panics(t, func() {
u.Max([]int{})
})
}

4
min.go
View file

@ -3,13 +3,9 @@ package underscore
import "cmp"
// Min returns the minimum value in the slice.
// Panics if values is empty.
// This function can currently only compare numbers reliably.
// This function uses operator <.
func Min[T cmp.Ordered](values []T) T {
if len(values) == 0 {
panic("underscore.Min: empty slice")
}
min := values[0]
for _, v := range values {
if v < min {

View file

@ -13,9 +13,3 @@ func TestMin(t *testing.T) {
want := 1
assert.Equal(t, want, u.Min(nums))
}
func TestMinEmpty(t *testing.T) {
assert.Panics(t, func() {
u.Min([]int{})
})
}

View file

@ -1,21 +1,27 @@
package underscore
import "slices"
// 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
// Uses O(n log n) sorting algorithm. Mutates the input slice.
//
// func (left Person, right Person) bool { return left.Age > right.Age }
func OrderBy[T any](list []T, predicate func(T, T) bool) []T {
slices.SortFunc(list, func(a, b T) int {
if predicate(a, b) {
return 1
swaps := true
var tmp T
//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
}

View file

@ -29,31 +29,3 @@ func Test_OrderBy_Desc(t *testing.T) {
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 })
}
}

View file

@ -3,7 +3,6 @@ package underscore_test
import (
"context"
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@ -41,44 +40,3 @@ func TestParallelMap_DefaultWorkers(t *testing.T) {
assert.NoError(t, err)
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
})
}
})
}

View file

@ -3,8 +3,8 @@ package underscore
// Partition splits the slice into two slices: one whose elements all satisfy predicate
// and one whose elements all do not satisfy predicate.
func Partition[T any](values []T, predicate func(T) bool) ([]T, []T) {
keep := make([]T, 0, len(values))
reject := make([]T, 0, len(values))
keep := make([]T, 0)
reject := make([]T, 0)
for _, v := range values {
if predicate(v) {

View file

@ -20,41 +20,3 @@ func TestPartition(t *testing.T) {
assert.Equal(t, wantEvens, evens)
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 })
}
}

View file

@ -17,25 +17,3 @@ func TestReduce(t *testing.T) {
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)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -14,50 +14,3 @@ func TestUnique(t *testing.T) {
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)
}
}