mirror of
https://github.com/rjNemo/underscore
synced 2026-06-12 05:26:39 +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
|
||||
|
||||
```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.
|
||||
|
|
@ -92,22 +92,33 @@ make test
|
|||
|
||||
- `All`
|
||||
- `Any`
|
||||
- `Chunk`
|
||||
- `Contains`
|
||||
- `ContainsBy`
|
||||
- `Count`
|
||||
- `Difference`
|
||||
- `Drop`
|
||||
- `Each`
|
||||
- `Filter`
|
||||
- `Find`
|
||||
- `Flatmap`
|
||||
- `GroupBy`
|
||||
- `Find`
|
||||
- `Intersection`
|
||||
- `Join` / `JoinProject`
|
||||
- `Last`
|
||||
- `Map`
|
||||
- `Max`
|
||||
- `Min`
|
||||
- `OrderBy`
|
||||
- `Partition`
|
||||
- `Range`
|
||||
- `Reduce`
|
||||
- `RemoveAt`
|
||||
- `Sum` / `SumMap`
|
||||
- `Unique`
|
||||
- `UniqueBy`
|
||||
- `UniqueInPlace`
|
||||
- `Chunk`
|
||||
- `Zip`
|
||||
|
||||
### 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
|
||||
|
||||
- `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
|
||||
|
||||
|
|
|
|||
20
drop.go
20
drop.go
|
|
@ -1,12 +1,16 @@
|
|||
package underscore
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
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) {
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||
want := []int{1, 9, 2, 3, 7, 4, 6, 5}
|
||||
nums := []int{1, 2, 3, 4, 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).
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -15,3 +15,40 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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 {
|
||||
vs := mapper(v)
|
||||
res = append(res, vs...)
|
||||
|
|
|
|||
|
|
@ -15,3 +15,16 @@ 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
5
go.mod
|
|
@ -2,10 +2,7 @@ module github.com/rjNemo/underscore
|
|||
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc
|
||||
)
|
||||
require github.com/stretchr/testify v1.8.4
|
||||
|
||||
require (
|
||||
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/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=
|
||||
|
|
|
|||
|
|
@ -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, 0)
|
||||
res := make(map[K][]V, len(values)/10)
|
||||
for _, v := range values {
|
||||
k := f(v)
|
||||
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)
|
||||
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: two, Right: []u.Tuple[int, string]{two, two}},
|
||||
{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
|
||||
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}
|
||||
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"
|
||||
|
||||
// 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 {
|
||||
|
|
|
|||
|
|
@ -13,3 +13,9 @@ 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
4
min.go
|
|
@ -3,9 +3,13 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -13,3 +13,9 @@ 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{})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
28
orderBy.go
28
orderBy.go
|
|
@ -1,27 +1,21 @@
|
|||
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 {
|
||||
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
|
||||
}
|
||||
slices.SortFunc(list, func(a, b T) int {
|
||||
if predicate(a, b) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if predicate(b, a) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,3 +29,31 @@ 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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package underscore_test
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -40,3 +41,44 @@ 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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
reject := make([]T, 0)
|
||||
keep := make([]T, 0, len(values))
|
||||
reject := make([]T, 0, len(values))
|
||||
|
||||
for _, v := range values {
|
||||
if predicate(v) {
|
||||
|
|
|
|||
|
|
@ -20,3 +20,41 @@ 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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,3 +17,25 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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))
|
||||
}
|
||||
|
||||
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