mirror of
https://github.com/rjNemo/underscore
synced 2026-06-06 10:36:43 +00:00
Compare commits
No commits in common. "main" and "v0.4.0" have entirely different histories.
146 changed files with 257 additions and 4098 deletions
10
.github/workflows/unit-test.yml
vendored
10
.github/workflows/unit-test.yml
vendored
|
|
@ -1,5 +1,5 @@
|
|||
name: Tests
|
||||
on: [push, pull_request]
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
|
|
@ -8,16 +8,16 @@ jobs:
|
|||
- uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: '1.18'
|
||||
- name: Run tests with coverage
|
||||
run: go test -coverprofile=coverage.out -covermode=count ./...
|
||||
- uses: codecov/codecov-action@v4
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.out
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
verbose: true
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -59,8 +59,3 @@ Temporary Items
|
|||
.apdisk
|
||||
docs/public
|
||||
.trivycache/
|
||||
.vscode/launch.json
|
||||
.claude
|
||||
AGENTS.md
|
||||
bench*txt
|
||||
ACTION_PLAN.md
|
||||
|
|
|
|||
|
|
@ -1,54 +1,42 @@
|
|||
version: "2"
|
||||
skip-dirs-use-default: true
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- dogsled
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- goprintffuncname
|
||||
- gosimple
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- rowserrcheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
list-mode: lax
|
||||
files:
|
||||
- $all
|
||||
allow:
|
||||
- $gostd
|
||||
- github.com/rjNemo/underscore
|
||||
- github.com/rjNemo/underscore/...
|
||||
- github.com/stretchr/testify/...
|
||||
- golang.org/x/exp/constraints
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
fast: true
|
||||
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/rjNemo/underscore
|
||||
183
CLAUDE.md
183
CLAUDE.md
|
|
@ -1,183 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
`underscore` is a Go library providing functional programming helpers inspired by underscore.js, built on Go 1.18+ generics. The library is organized as a flat structure with individual files for each function, plus a `maps` subpackage for map-specific utilities.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Testing
|
||||
|
||||
```sh
|
||||
# Run all tests (local)
|
||||
go test ./...
|
||||
|
||||
# Run all tests with coverage (local)
|
||||
go test ./... -coverpkg=./... -coverprofile cov.out -covermode=count
|
||||
go tool cover -func cov.out
|
||||
rm cov.out
|
||||
|
||||
# Run tests in Docker (preferred for CI/validation)
|
||||
make test
|
||||
|
||||
# Run a single test
|
||||
go test -run TestFunctionName
|
||||
|
||||
# Run tests for a specific file
|
||||
go test -run TestMap
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```sh
|
||||
# Build Docker image
|
||||
make build
|
||||
|
||||
# Install dependencies
|
||||
go mod download
|
||||
```
|
||||
|
||||
### Linting & Security
|
||||
|
||||
```sh
|
||||
# Scan Docker image for vulnerabilities
|
||||
make scan
|
||||
|
||||
# Scan config files
|
||||
make scan-config
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
```sh
|
||||
# Serve docs locally at http://localhost:1313
|
||||
make docs
|
||||
|
||||
# Build static docs
|
||||
make build-docs
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Code Organization
|
||||
|
||||
The library uses a **flat structure** where each function is implemented in its own file:
|
||||
|
||||
- `<function>.go` - implementation
|
||||
- `<function>_test.go` - tests
|
||||
|
||||
Example: `filter.go` + `filter_test.go`, `map.go` + `map_test.go`
|
||||
|
||||
### Core Patterns
|
||||
|
||||
**Generic Functions**: Most functions use Go generics with constraints from `cmp.Ordered` or custom type parameters. Functions operate on slices and return new slices (immutable style).
|
||||
|
||||
**Pipe Chain**: The `Pipe[T]` struct enables method chaining for ordered types. Methods that return slices continue the chain, while methods that return values (like `All`, `Any`, `Reduce`) break the chain and return the final value.
|
||||
|
||||
```go
|
||||
// pipe.go defines Pipe[T cmp.Ordered]
|
||||
// Chain-continuing: Filter, Map
|
||||
// Chain-breaking: All, Any, Reduce, Min, Max, Partition, Find, Each
|
||||
```
|
||||
|
||||
**Concurrency Helpers**: `ParallelMap` and `ParallelFilter` use worker pools with:
|
||||
|
||||
- Context-based cancellation
|
||||
- Order preservation (results match input order)
|
||||
- First-error-wins semantics
|
||||
- Default workers = GOMAXPROCS if workers <= 0
|
||||
|
||||
Implementation detail: Uses `sync.Once` to capture first error and cancel context immediately.
|
||||
|
||||
**Subpackages**:
|
||||
|
||||
- `maps/` - Map-specific utilities (`Keys`, `Values`, `Map`)
|
||||
- Uses type alias `M[K, V] = map[K]V` for cleaner signatures
|
||||
- `Map` function allows transforming map entries
|
||||
|
||||
### Testing Conventions
|
||||
|
||||
- Use `testify/assert` for assertions
|
||||
- Test file names match source files with `_test.go` suffix
|
||||
- Table-driven tests are common (see `map_test.go`, `filter_test.go`)
|
||||
- Internal tests (using `package underscore` rather than `package underscore_test`) are used sparingly for testing unexported functions
|
||||
|
||||
## Key Constraints
|
||||
|
||||
- **Minimum Go version**: 1.24.2 (see go.mod)
|
||||
- **Generic constraints**: Most collection functions require `cmp.Ordered` types; some use `comparable` or no constraints
|
||||
- **Order preservation**: `ParallelMap` and `ParallelFilter` guarantee output order matches input order
|
||||
- **No mutation**: Functions return new slices; `UniqueInPlace` is the exception (in-place deduplication)
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### Recently Fixed (2025-11-14)
|
||||
|
||||
1. ✅ **Filter allocation** - Now pre-allocates with `make([]T, 0, len(values))` (90% fewer allocations)
|
||||
2. ✅ **OrderBy algorithm** - Replaced bubble sort with `slices.SortFunc` (629x faster for large datasets)
|
||||
3. ✅ **Partition allocation** - Now pre-allocates both result slices
|
||||
4. ✅ **Max/Min empty slices** - Now panics with clear message: "underscore.Max: empty slice"
|
||||
5. ✅ **Drop semantics** - Fixed to drop first N elements (breaking change). Old behavior available as `RemoveAt`
|
||||
|
||||
### API Design Issues
|
||||
|
||||
1. **Pipe constraint** (`pipe.go:7`) - `Pipe[T cmp.Ordered]` prevents usage with custom types
|
||||
2. **Last panics** (`last.go:5-8`) - No empty slice handling
|
||||
|
||||
### Missing Features
|
||||
|
||||
Popular FP utilities not yet implemented: `TakeWhile`, `DropWhile`, `Scan`, `First/FirstN`, `Init`, `Intersperse`, `Sliding`, `FoldRight`, `Tap`, `Transpose`, `Unzip`, `ParallelReduce`, `Replicate`
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Good Performance Patterns
|
||||
- `Filter` pre-allocates: `make([]T, 0, len(values))` ✅ (Fixed 2025-11-14)
|
||||
- `Map` pre-allocates: `make([]P, 0, len(values))`
|
||||
- `Partition` pre-allocates: `make([]T, 0, len(values))` for both slices ✅ (Fixed 2025-11-14)
|
||||
- `Chunk` pre-calculates capacity: `(l+n-1)/n`
|
||||
- `ParallelFilter` pre-counts before allocation
|
||||
- `OrderBy` uses `slices.SortFunc`: O(n log n) performance ✅ (Fixed 2025-11-14)
|
||||
|
||||
### Remaining Performance Issues
|
||||
- `Flatmap`: Accumulation overhead from repeated appends
|
||||
- `GroupBy`: Map initialized with capacity 0 (useless hint)
|
||||
|
||||
### When to Use ParallelMap vs Map
|
||||
|
||||
Use `ParallelMap` when:
|
||||
- Processing 100+ elements with expensive operations (>1ms per element)
|
||||
- Operations are CPU-bound (not I/O-bound with shared resources)
|
||||
- Order preservation is required
|
||||
- Context cancellation is needed
|
||||
|
||||
Use regular `Map` when:
|
||||
- Small slices (<100 elements)
|
||||
- Fast operations (<100µs per element)
|
||||
- Avoiding goroutine overhead matters
|
||||
- Simple transformations without error handling
|
||||
|
||||
**Worker count guidelines:**
|
||||
- Default (workers=0): Uses `runtime.GOMAXPROCS(0)` - good starting point
|
||||
- CPU-bound: Use GOMAXPROCS or GOMAXPROCS*2
|
||||
- I/O-bound: Can use higher values (10-100) if not sharing resources
|
||||
|
||||
## Contributing Notes
|
||||
|
||||
When adding new functions:
|
||||
|
||||
1. Create both `<function>.go` and `<function>_test.go`
|
||||
2. Add examples in comments using Go doc format
|
||||
3. Pre-allocate slices with `make([]T, 0, len(input))` when output size is similar to input
|
||||
4. Document panic conditions (empty slices, nil inputs, invalid indices)
|
||||
5. Add edge case tests (empty, single element, nil)
|
||||
6. If the function applies to Pipe chains, add a method to `pipe.go`
|
||||
7. Update README.md function list if adding new collection functions
|
||||
8. Follow SemVer for version numbers
|
||||
9. Ensure all tests pass: `make test`
|
||||
|
||||
When fixing bugs:
|
||||
- Add regression tests before fixing
|
||||
- Run benchmarks if performance-related: `go test -bench=. -benchmem`
|
||||
- Check for similar issues in other functions
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.24.2-alpine
|
||||
FROM golang:1.18-alpine
|
||||
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GOOS linux
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
FROM golang:1.24.2-alpine AS builder
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
|
||||
RUN apk -U upgrade --no-cache
|
||||
RUN apk add hugo
|
||||
|
||||
WORKDIR /docs
|
||||
|
||||
COPY go.* ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN hugo --gc --minify
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /docs/public /public
|
||||
3
Makefile
3
Makefile
|
|
@ -6,7 +6,7 @@ build:
|
|||
docker build -t $(IMAGE):latest .
|
||||
|
||||
test: build
|
||||
docker run --name $(IMAGE) --rm -i $(IMAGE) sh -c "$(TEST) $(COVER)"
|
||||
docker run --name $(IMAGE) --rm -i -t $(IMAGE) sh -c "$(TEST) $(COVER)"
|
||||
|
||||
scan:
|
||||
trivy --cache-dir .trivycache/ image --exit-code 0 --no-progress --severity CRITICAL $(IMAGE)
|
||||
|
|
@ -14,7 +14,6 @@ scan:
|
|||
scan-config:
|
||||
trivy config .
|
||||
|
||||
.PHONY: docs
|
||||
docs:
|
||||
cd docs && hugo server -D
|
||||
|
||||
|
|
|
|||
142
README.md
142
README.md
|
|
@ -1,18 +1,15 @@
|
|||
# \_Underscore
|
||||
# _Underscore
|
||||
|
||||

|
||||
[](https://pkg.go.dev/github.com/rjNemo/underscore)
|
||||

|
||||

|
||||
[](https://www.bestpractices.dev/projects/9726)
|
||||
|
||||

|
||||
|
||||
`underscore` is a `Go` library providing useful functional programming helpers without
|
||||
extending any built-in objects.
|
||||
`underscore` is a `Go` library providing useful functional programming helpers without extending any built-in objects.
|
||||
|
||||
It is mostly a port from the `underscore.js` library based on generics brought by
|
||||
`Go 1.18`.
|
||||
It is mostly a port from the `underscore.js` library based on generics brought by `Go 1.18`.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -20,8 +17,8 @@ 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
|
||||
```shell
|
||||
go get github.com/rjNemo/underscore@0.4.0
|
||||
```
|
||||
|
||||
Please check out the [examples](examples) to see how to use the library.
|
||||
|
|
@ -30,47 +27,46 @@ Please check out the [examples](examples) to see how to use the library.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
// filter even numbers from the slice
|
||||
evens := u.Filter(numbers, func(n int) bool { return n%2 == 0 })
|
||||
// square every number in the slice
|
||||
squares := u.Map(evens, func(n int) int { return n * n })
|
||||
// reduce to the sum
|
||||
res := u.Reduce(squares, func(n, acc int) int { return n + acc }, 0)
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
// filter even numbers from the slice
|
||||
evens := u.Filter(numbers, func(n int) bool { return n%2 == 0 })
|
||||
// square every number in the slice
|
||||
squares := u.Map(evens, func(n int) int { return n * n })
|
||||
// reduce to the sum
|
||||
res := u.Reduce(squares, func(n, acc int) int { return n + acc }, 0)
|
||||
|
||||
fmt.Println(res) // 120
|
||||
fmt.Println(res) // 120
|
||||
}
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
These instructions will get you a copy of the project up and running on your local
|
||||
machine for development and testing purposes.
|
||||
These instructions will get you a copy of the project up and running on your local machine for development and testing
|
||||
purposes.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You need at least `go1.24` for development. The project is shipped with a [Dockerfile](Dockerfile)
|
||||
based on `go1.24`.
|
||||
You need at least `go1.18` for development. The project is shipped with a [Dockerfile](Dockerfile) based on `go1.18`.
|
||||
|
||||
If you prefer local development, navigate to the [official
|
||||
download page](https://go.dev/dl/) and install version `1.24` or beyond.
|
||||
download page](https://go.dev/dl/) and install version `1.18` or beyond.
|
||||
|
||||
### Installing
|
||||
|
||||
First clone the repository
|
||||
|
||||
```sh
|
||||
```shell
|
||||
git clone https://github.com/rjNemo/underscore.git
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```sh
|
||||
```shell
|
||||
go mod download
|
||||
```
|
||||
|
||||
|
|
@ -80,7 +76,7 @@ And that's it.
|
|||
|
||||
To run the unit tests, you can simply run:
|
||||
|
||||
```sh
|
||||
```shell
|
||||
make test
|
||||
```
|
||||
|
||||
|
|
@ -92,94 +88,22 @@ make test
|
|||
|
||||
- `All`
|
||||
- `Any`
|
||||
- `Chunk`
|
||||
- `Contains`
|
||||
- `ContainsBy`
|
||||
- `Count`
|
||||
- `Difference`
|
||||
- `Drop`
|
||||
- `Contains` (only numerics values at the moment)
|
||||
- `Each`
|
||||
- `Filter`
|
||||
- `Find`
|
||||
- `Flatmap`
|
||||
- `GroupBy`
|
||||
- `Intersection`
|
||||
- `Join` / `JoinProject`
|
||||
- `Last`
|
||||
- `Map`
|
||||
- `Max`
|
||||
- `Min`
|
||||
- `OrderBy`
|
||||
- `Partition`
|
||||
- `Range`
|
||||
- `Reduce`
|
||||
- `RemoveAt`
|
||||
- `Sum` / `SumMap`
|
||||
- `Unique`
|
||||
- `UniqueBy`
|
||||
- `UniqueInPlace`
|
||||
- `Zip`
|
||||
|
||||
### Pipe
|
||||
|
||||
Calling `NewPipe` will cause all future method calls to return wrapped values. When
|
||||
you've finished the computation, call `Value` to retrieve the final value.
|
||||
Calling `NewPipe` will cause all future method calls to return wrapped values. When you've finished the computation,
|
||||
call `Value` to retrieve the final value.
|
||||
|
||||
Methods not returning a slice such as `Reduce`, `All`, `Any`, will break the `Chain`
|
||||
and return `Value` instantly.
|
||||
|
||||
### Concurrency
|
||||
|
||||
- `ParallelMap(ctx, values, workers, fn)`: apply a function concurrently while preserving order and supporting context cancellation.
|
||||
- `ParallelFilter(ctx, values, workers, fn)`: filter concurrently with order preserved and context support.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
out, err := u.ParallelMap(context.Background(), []int{1, 2, 3, 4}, 4,
|
||||
func(ctx context.Context, n int) (int, error) { return n * n, nil },
|
||||
)
|
||||
fmt.Println(out, err) // [1 4 9 16] <nil>
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// ParallelFilter example
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
out, err := u.ParallelFilter(context.Background(), []int{1,2,3,4,5}, 3,
|
||||
func(ctx context.Context, n int) (bool, error) { return n%2==0, nil },
|
||||
)
|
||||
fmt.Println(out, err) // [2 4] <nil>
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
Methods not returning a slice such as `Reduce`, `All`, `Any`, will break the `Chain` and return `Value` instantly.
|
||||
|
||||
## Built With
|
||||
|
||||
|
|
@ -187,8 +111,8 @@ func main() {
|
|||
|
||||
## Contributing
|
||||
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct,
|
||||
and the process for submitting pull requests to us.
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull
|
||||
requests to us.
|
||||
|
||||
## Versioning
|
||||
|
||||
|
|
@ -199,15 +123,13 @@ the [tags on this repository](https://github.com/rjNemo/underscore/tags).
|
|||
|
||||
- **Ruidy** - _Initial work_ - [Ruidy](https://github.com/rjNemo)
|
||||
|
||||
See also the list of [contributors](https://github.com/rjNemo/underscore/contributors)
|
||||
who participated in this project.
|
||||
See also the list of [contributors](https://github.com/rjNemo/underscore/contributors) who participated in this project.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md)
|
||||
file for details
|
||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
This project is largely inspired by [Underscore.js](https://underscorejs.org/#)
|
||||
library. Check out the original project if you don't already know it.
|
||||
This project is largely inspired by [Underscore.js](https://underscorejs.org/#) library. Check out the original project
|
||||
if you don't already know it.
|
||||
|
|
|
|||
9
any.go
9
any.go
|
|
@ -1,9 +1,12 @@
|
|||
package underscore
|
||||
|
||||
import "slices"
|
||||
|
||||
// Any returns true if any of the values in the slice pass the predicate truth test.
|
||||
// Short-circuits and stops traversing the slice if a true element is found.
|
||||
func Any[T any](values []T, predicate func(T) bool) bool {
|
||||
return slices.ContainsFunc(values, predicate)
|
||||
for _, v := range values {
|
||||
if predicate(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
19
chunk.go
19
chunk.go
|
|
@ -1,19 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Chunk splits the input slice into groups of size n.
|
||||
// If n <= 0, it returns nil. The final chunk may be smaller than n.
|
||||
func Chunk[T any](values []T, n int) [][]T {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
l := len(values)
|
||||
if l == 0 {
|
||||
return [][]T{}
|
||||
}
|
||||
chunks := make([][]T, 0, (l+n-1)/n)
|
||||
for i := 0; i < l; i += n {
|
||||
j := min(i+n, l)
|
||||
chunks = append(chunks, values[i:j])
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestChunk(t *testing.T) {
|
||||
in := []int{1, 2, 3, 4, 5}
|
||||
got := u.Chunk(in, 2)
|
||||
want := [][]int{{1, 2}, {3, 4}, {5}}
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func TestChunkLargeSize(t *testing.T) {
|
||||
in := []int{1, 2, 3}
|
||||
got := u.Chunk(in, 10)
|
||||
want := [][]int{{1, 2, 3}}
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func TestChunkInvalidSize(t *testing.T) {
|
||||
var in []int
|
||||
assert.Nil(t, u.Chunk(in, 0))
|
||||
assert.Nil(t, u.Chunk(in, -1))
|
||||
}
|
||||
|
||||
func TestChunkEmpty(t *testing.T) {
|
||||
got := u.Chunk([]int{}, 1)
|
||||
assert.Equal(t, 0, len(got))
|
||||
}
|
||||
14
contains.go
14
contains.go
|
|
@ -1,13 +1,11 @@
|
|||
package underscore
|
||||
|
||||
import "slices"
|
||||
|
||||
// Contains returns true if the value is present in the slice
|
||||
func Contains[T comparable](values []T, value T) bool {
|
||||
return slices.Contains(values, value)
|
||||
}
|
||||
|
||||
// ContainsBy returns true if any element in the slice satisfies the predicate.
|
||||
func ContainsBy[T any](values []T, predicate func(T) bool) bool {
|
||||
return slices.ContainsFunc(values, predicate)
|
||||
for _, v := range values {
|
||||
if v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,19 +17,3 @@ func TestNotContains(t *testing.T) {
|
|||
nums := []int{1, 3, 5, 7, 9}
|
||||
assert.False(t, u.Contains(nums, 15))
|
||||
}
|
||||
|
||||
func TestContainsBy(t *testing.T) {
|
||||
nums := []int{1, 3, 5, 8}
|
||||
assert.True(t, u.ContainsBy(nums, func(n int) bool { return n%2 == 0 }))
|
||||
assert.False(t, u.ContainsBy(nums, func(n int) bool { return n < 0 }))
|
||||
}
|
||||
|
||||
func TestContainsByStruct(t *testing.T) {
|
||||
type user struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
users := []user{{1, "a"}, {2, "b"}, {3, "c"}}
|
||||
assert.True(t, u.ContainsBy(users, func(u user) bool { return u.ID == 2 }))
|
||||
assert.False(t, u.ContainsBy(users, func(u user) bool { return u.Name == "z" }))
|
||||
}
|
||||
|
|
|
|||
13
count.go
13
count.go
|
|
@ -1,13 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Count returns the number of elements in the slice that satisfy the predicate.
|
||||
// example: Count([]int{1,2,3,4,5}, func(n int) bool { return n%2 == 0 }) // 2
|
||||
func Count[T any](slice []T, predicate func(T) bool) int {
|
||||
count := 0
|
||||
for _, item := range slice {
|
||||
if predicate(item) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package underscore
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Count_Can_Count_Numbers(t *testing.T) {
|
||||
numbers := Range(1, 100)
|
||||
count := Count(numbers, func(n int) bool {
|
||||
return n%2 == 0
|
||||
})
|
||||
|
||||
assert.Equal(t, 50, count)
|
||||
}
|
||||
|
||||
type People struct {
|
||||
Name string
|
||||
Age int
|
||||
Gender string
|
||||
}
|
||||
|
||||
func Test_Count_Can_Count_People(t *testing.T) {
|
||||
people := []People{
|
||||
{Name: "Andy", Age: 43, Gender: "M"},
|
||||
{Name: "Fred", Age: 33, Gender: "M"},
|
||||
{Name: "Jack", Age: 23, Gender: "M"},
|
||||
{Name: "Jill", Age: 43, Gender: "F"},
|
||||
{Name: "Anna", Age: 33, Gender: "F"},
|
||||
{Name: "Arya", Age: 23, Gender: "F"},
|
||||
{Name: "Jane", Age: 13, Gender: "F"},
|
||||
}
|
||||
|
||||
a := Count(people, func(p People) bool {
|
||||
return strings.HasPrefix(p.Name, "A")
|
||||
})
|
||||
assert.Equal(t, 3, a)
|
||||
|
||||
females := Count(people, func(p People) bool {
|
||||
return p.Gender == "F"
|
||||
})
|
||||
assert.Equal(t, 4, females)
|
||||
|
||||
males := Count(people, func(p People) bool {
|
||||
return p.Gender == "M"
|
||||
})
|
||||
assert.Equal(t, 3, males)
|
||||
|
||||
over30 := Count(people, func(p People) bool {
|
||||
return p.Age > 30
|
||||
})
|
||||
assert.Equal(t, 4, over30)
|
||||
|
||||
under30 := Count(people, func(p People) bool {
|
||||
return p.Age < 30
|
||||
})
|
||||
assert.Equal(t, 3, under30)
|
||||
|
||||
under20 := Count(people, func(p People) bool {
|
||||
return p.Age < 20
|
||||
})
|
||||
assert.Equal(t, 1, under20)
|
||||
}
|
||||
|
|
@ -9,11 +9,10 @@ title: _Underscore
|
|||
|
||||

|
||||
|
||||
`underscore` is a `Go` library providing useful functional programming helpers without
|
||||
extending any built-in objects.
|
||||
|
||||
It is mostly a port from the `underscore.js` library based on generics available
|
||||
from `go1.18`.
|
||||
`underscore` is a `Go` library providing useful functional programming helpers without extending any built-in objects.
|
||||
|
||||
It is mostly a port from the `underscore.js` library based on generics brought by `go1.18`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
@ -23,26 +22,25 @@ Install the library using
|
|||
go get github.com/rjNemo/underscore
|
||||
```
|
||||
|
||||
Please check out the [examples](https://github.com/rjNemo/underscore/tree/main/examples)
|
||||
to see how to use the library.
|
||||
Please check out the [examples](https://github.com/rjNemo/underscore/tree/main/examples) to see how to use the library.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
// filter even numbers from the slice
|
||||
evens := u.Filter(numbers, func(n int) bool { return n%2 == 0 })
|
||||
// square every number in the slice
|
||||
squares := u.Map(evens, func(n int) int { return n * n })
|
||||
// reduce to the sum
|
||||
res := u.Reduce(squares, func(n, acc int) int { return n + acc }, 0)
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
// filter even numbers from the slice
|
||||
evens := u.Filter(numbers, func(n int) bool { return n%2 == 0 })
|
||||
// square every number in the slice
|
||||
squares := u.Map(evens, func(n int) int { return n * n })
|
||||
// reduce to the sum
|
||||
res := u.Reduce(squares, func(n, acc int) int { return n + acc }, 0)
|
||||
|
||||
fmt.Println(res) // 120
|
||||
fmt.Println(res) // 120
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ title: "All"
|
|||
date: 2021-12-30T13:24:39-04:00
|
||||
---
|
||||
|
||||
|
||||
`All` returns true if all the values in the slice pass the predicate truth test.\
|
||||
Short-circuits and stops traversing the slice if a false element is found.
|
||||
|
||||
|
|
@ -10,13 +11,13 @@ Short-circuits and stops traversing the slice if a false element is found.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 3, 5, 7, 9}
|
||||
isOdd := func(n int) bool { return n%2 != 0 }
|
||||
fmt.Println(u.All(nums, isOdd)) // true
|
||||
nums := []int{1, 3, 5, 7, 9}
|
||||
isOdd := func(n int) bool { return n%2 != 0 }
|
||||
fmt.Println(u.All(nums, isOdd)) // true
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ the slice if a true element is found.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 4, 6, 8}
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
fmt.Println(u.Any(nums, isEven)) // true
|
||||
nums := []int{1, 2, 4, 6, 8}
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
fmt.Println(u.Any(nums, isEven)) // true
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
title: "Chunk"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Chunk` splits a slice into groups of size `n`. The last chunk may be smaller.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(u.Chunk([]int{1,2,3,4,5}, 2)) // [[1 2] [3 4] [5]]
|
||||
}
|
||||
```
|
||||
|
|
@ -9,13 +9,13 @@ date: 2022-03-21T13:30:29-04:00
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 3, 5, 7, 9}
|
||||
nums := []int{1, 3, 5, 7, 9}
|
||||
|
||||
fmt.Println(u.Contains(nums, 5)) // true
|
||||
fmt.Println(u.Contains(nums, 5)) // true
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
title: "ContainsBy"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
`ContainsBy` returns true if any element satisfies the predicate.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 3, 5, 8}
|
||||
fmt.Println(u.ContainsBy(nums, func(n int) bool { return n%2 == 0 })) // true
|
||||
}
|
||||
```
|
||||
|
|
@ -9,14 +9,14 @@ Returns a copy of the array with all instances of the values that are not presen
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 3, 5, 6, 7, 9}
|
||||
reject := []int{9, 7, 5, 4}
|
||||
|
||||
fmt.Println(u.Difference(nums, reject)) // {1, 3, 6}
|
||||
nums := []int{1, 3, 5, 6, 7, 9}
|
||||
reject := []int{9, 7, 5, 4}
|
||||
|
||||
fmt.Println(u.Difference(nums, reject)) // {1, 3, 6}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ onward.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||
|
||||
fmt.Println(u.Drop(nums, 3)) // {1, 9, 2, 3, 7, 4, 6, 5}
|
||||
fmt.Println(u.Drop(nums, 3)) // {1, 9, 2, 3, 7, 4, 6, 5}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
title: "DropWhile"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`DropWhile` drops elements from the beginning of the slice while the predicate returns true. It returns the remaining elements starting from the first element where the predicate returns false.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
lessThan5 := func(n int) bool { return n < 5 }
|
||||
fmt.Println(u.DropWhile(nums, lessThan5)) // [5, 6, 7, 8, 9]
|
||||
|
||||
words := []string{"apple", "banana", "cherry", "date"}
|
||||
shortWords := func(s string) bool { return len(s) < 6 }
|
||||
fmt.Println(u.DropWhile(words, shortWords)) // ["banana", "cherry", "date"]
|
||||
}
|
||||
```
|
||||
|
|
@ -9,17 +9,17 @@ date: 2022-03-21T13:30:59-04:00
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
names := []string{"Alice", "Bob", "Charles"}
|
||||
res := make([]string, 0)
|
||||
names := []string{"Alice", "Bob", "Charles"}
|
||||
res := make([]string, 0)
|
||||
|
||||
u.Each(names, func(n string) {
|
||||
res = append(res, fmt.Sprintf("Hi %s", n))
|
||||
})
|
||||
fmt.Println(res) // {"Hi Alice", "Hi Bob", "Hi Charles"}
|
||||
u.Each(names, func(n string) {
|
||||
res = append(res, fmt.Sprintf("Hi %s", n))
|
||||
})
|
||||
fmt.Println(res) // {"Hi Alice", "Hi Bob", "Hi Charles"}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ date: 2022-03-21T13:31:21-04:00
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
fmt.Println(u.Filter(nums, isEven)) // {0, 2, 4, 6, 8}
|
||||
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
fmt.Println(u.Filter(nums, isEven)) // {0, 2, 4, 6, 8}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,16 +11,16 @@ element, and doesn't traverse the entire slice.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{2, 4, 5, 6, 8, 0}
|
||||
isOdd := func(n int) bool { return n%2 != 0 }
|
||||
nums := []int{2, 4, 5, 6, 8, 0}
|
||||
isOdd := func(n int) bool { return n%2 != 0 }
|
||||
|
||||
n, err := u.Find(nums, isOdd)
|
||||
fmt.Println(n) // 5
|
||||
fmt.Println(err) // nil
|
||||
n, err := u.Find(nums, isOdd)
|
||||
fmt.Println(n) // 5
|
||||
fmt.Println(err) // nil
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
title: "First"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`First` returns the first element of the slice. Returns an error if the slice is empty.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
first, err := u.First(nums)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(first) // 1
|
||||
|
||||
// Handle empty slice
|
||||
empty := []int{}
|
||||
_, err = u.First(empty)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err) // Error: underscore: empty slice
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
title: "FirstN"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`FirstN` returns the first n elements of the slice. If n is greater than the slice length, returns the entire slice. If n is less than or equal to 0, returns an empty slice.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
fmt.Println(u.FirstN(nums, 3)) // [1, 2, 3]
|
||||
fmt.Println(u.FirstN(nums, 0)) // []
|
||||
fmt.Println(u.FirstN(nums, 10)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
fmt.Println(u.FirstN(nums, -5)) // []
|
||||
}
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
title: "Flatmap"
|
||||
date: 2022-08-10T16:49:56+02:00
|
||||
draft: false
|
||||
---
|
||||
|
||||
Flatmap flattens 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.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4}
|
||||
mapper := func(n int) []int { return []int{(n - 1) * n, (n) * n} }
|
||||
res := u.Flatmap(nums, mapper)
|
||||
fmt.Println(res) // {0, 1, 2, 4, 6, 9, 12, 16}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
---
|
||||
title: "FoldRight"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`FoldRight` is like Reduce but processes elements from right to left. Also known as foldr in Haskell. Important for non-associative operations where the order of evaluation matters.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Subtraction is non-associative
|
||||
nums := []int{1, 2, 3}
|
||||
|
||||
// FoldRight: 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2
|
||||
result := u.FoldRight(nums, 0, func(n, acc int) int { return n - acc })
|
||||
fmt.Println(result) // 2
|
||||
|
||||
// Compare with Reduce (left fold): (0 - 1) - 2 - 3 = -6
|
||||
leftResult := u.Reduce(nums, func(n, acc int) int { return acc - n }, 0)
|
||||
fmt.Println(leftResult) // -6
|
||||
|
||||
// Building a list in order
|
||||
buildList := u.FoldRight(nums, []int{}, func(n int, acc []int) []int {
|
||||
return append([]int{n}, acc...)
|
||||
})
|
||||
fmt.Println(buildList) // [1, 2, 3]
|
||||
|
||||
// String concatenation
|
||||
words := []string{"a", "b", "c"}
|
||||
concat := u.FoldRight(words, "", func(s, acc string) string { return s + acc })
|
||||
fmt.Println(concat) // "abc"
|
||||
}
|
||||
```
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
title: "Group by"
|
||||
date: 2023-06-07T00:49:56+02:00
|
||||
---
|
||||
|
||||
GroupBy splits a slice into a map[K][]V grouped by the result of the iterator function.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []float64{1.3, 2.1, 2.4}
|
||||
groupingFunc := func(n float64) int { return int(math.Floor(n)) }
|
||||
res := u.GroupBy(nums, groupingFunc) // { 1: {1.3}, 2: {2.1, 2.4}}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
title: "Init"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Init` returns all elements except the last one, and the last element separately. Returns an empty slice and zero value if the input slice is empty. Useful for destructuring lists from the right.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
init, last := u.Init(nums)
|
||||
fmt.Println(init) // [1, 2, 3, 4]
|
||||
fmt.Println(last) // 5
|
||||
|
||||
// Single element
|
||||
single, val := u.Init([]int{42})
|
||||
fmt.Println(single) // []
|
||||
fmt.Println(val) // 42
|
||||
|
||||
// Empty slice
|
||||
empty, zero := u.Init([]int{})
|
||||
fmt.Println(empty) // []
|
||||
fmt.Println(zero) // 0
|
||||
}
|
||||
```
|
||||
|
|
@ -11,8 +11,8 @@ package main
|
|||
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main(){
|
||||
|
|
@ -21,5 +21,4 @@ func main(){
|
|||
|
||||
fmt.Println(u.Intersection(a, b)) // {3, 5}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
---
|
||||
title: "Intersperse"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Intersperse` inserts a separator between each element of the slice. Returns an empty slice if the input is empty. Returns the original element if the input has only one element.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
fmt.Println(u.Intersperse(nums, 0)) // [1, 0, 2, 0, 3, 0, 4, 0, 5]
|
||||
|
||||
// Useful for formatting
|
||||
words := []string{"apple", "banana", "cherry"}
|
||||
fmt.Println(u.Intersperse(words, ",")) // ["apple", ",", "banana", ",", "cherry"]
|
||||
|
||||
// Single element - no separator added
|
||||
single := []int{42}
|
||||
fmt.Println(u.Intersperse(single, 0)) // [42]
|
||||
}
|
||||
```
|
||||
|
|
@ -3,7 +3,7 @@ title: "Last"
|
|||
date: 2022-03-21T13:46:24-04:00
|
||||
---
|
||||
|
||||
`Last` returns the last element of the slice. Panics if the slice is empty with a clear error message.
|
||||
`Last` returns the last element of the slice.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
|
@ -15,14 +15,7 @@ import (
|
|||
|
||||
func main() {
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||
|
||||
fmt.Println(u.Last(nums)) // 5
|
||||
|
||||
// Single element
|
||||
single := []int{42}
|
||||
fmt.Println(u.Last(single)) // 42
|
||||
|
||||
// Empty slice panics with clear message
|
||||
// empty := []int{}
|
||||
// u.Last(empty) // panic: underscore.Last: empty slice
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,22 +3,21 @@ title: "Map"
|
|||
date: 2022-03-21T13:32:10-04:00
|
||||
---
|
||||
|
||||
`Map` produces a new slice of values by mapping each value in the slice through a
|
||||
transform function.
|
||||
`Map` produces a new slice of values by mapping each value in the slice through a transform function.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3}
|
||||
toSquare := func(n int) int {
|
||||
return n * n
|
||||
}
|
||||
fmt.Println(u.Map(nums, toSquare)) // {1, 4, 9}
|
||||
nums := []int{1, 2, 3}
|
||||
toSquare := func(n int) int {
|
||||
return n * n
|
||||
}
|
||||
fmt.Println(u.Map(nums, toSquare)) // {1, 4, 9}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ uses operator `<`.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||
fmt.Println(u.Max(nums)) // 9
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||
fmt.Println(u.Max(nums)) // 9
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ uses operator `<`.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||
fmt.Println(u.Min(nums)) // 1
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 5}
|
||||
fmt.Println(u.Min(nums)) // 1
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
title: "ParallelFilter"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
`ParallelFilter` filters a slice concurrently with a worker pool, preserves order,
|
||||
and supports context cancellation.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
out, err := u.ParallelFilter(context.Background(), []int{1,2,3,4,5}, 3,
|
||||
func(ctx context.Context, n int) (bool, error) { return n%2==0, nil },
|
||||
)
|
||||
fmt.Println(out, err) // [2 4] <nil>
|
||||
}
|
||||
```
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
title: "ParallelMap"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
`ParallelMap` applies a function to each element concurrently using a worker pool,
|
||||
preserves order, and supports context cancellation.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
out, err := u.ParallelMap(context.Background(),
|
||||
[]int{1,2,3,4}, 4, func(ctx context.Context, n int) (int, error) {
|
||||
return n*n, nil
|
||||
})
|
||||
fmt.Println(out, err) // [1 4 9 16] <nil>
|
||||
}
|
||||
```
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
---
|
||||
title: "ParallelReduce"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`ParallelReduce` applies a reduction function in parallel using a worker pool. The operation must be associative and commutative for correct results. If workers <= 0, defaults to GOMAXPROCS. On error, the first error is returned and processing is canceled.
|
||||
|
||||
**Note:** This is an experimental function. Order of operations is not guaranteed, so use only with associative and commutative operations (like addition, multiplication, min, max).
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
ctx := context.Background()
|
||||
|
||||
// Parallel sum (safe - addition is associative and commutative)
|
||||
result, err := u.ParallelReduce(ctx, nums, 4, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
// Simulate expensive computation
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(result) // Result will vary due to parallel execution
|
||||
|
||||
// With context cancellation
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
_, err = u.ParallelReduce(ctx, nums, 4, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Operation was cancelled:", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Warning:** Do not use ParallelReduce for non-associative operations like subtraction or division, as the results will be unpredictable due to parallel execution order.
|
||||
|
|
@ -10,16 +10,16 @@ satisfy predicate.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
|
||||
evens, odds := u.Partition(nums, isEven)
|
||||
fmt.Println(evens) // {0, 2, 4, 6, 8}
|
||||
fmt.Println(odds) // {1, 3, 5, 7, 9}
|
||||
evens, odds := u.Partition(nums, isEven)
|
||||
fmt.Println(evens) // {0, 2, 4, 6, 8}
|
||||
fmt.Println(odds) // {1, 3, 5, 7, 9}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ be returned by the reduction function.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
sum := func(n, acc int) int { return n + acc }
|
||||
fmt.Println(u.Reduce(nums, sum, 0)) // 45
|
||||
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
sum := func(n, acc int) int { return n + acc }
|
||||
fmt.Println(u.Reduce(nums, sum, 0)) // 45
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
title: "Replicate"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Replicate` creates a slice containing count copies of value. Returns an empty slice if count is less than or equal to 0. Useful for initialization and testing.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Basic usage
|
||||
fmt.Println(u.Replicate(3, "hello"))
|
||||
// ["hello", "hello", "hello"]
|
||||
|
||||
// Numbers
|
||||
fmt.Println(u.Replicate(5, 0))
|
||||
// [0, 0, 0, 0, 0]
|
||||
|
||||
// Zero count
|
||||
fmt.Println(u.Replicate(0, 42))
|
||||
// []
|
||||
|
||||
// Negative count
|
||||
fmt.Println(u.Replicate(-5, "x"))
|
||||
// []
|
||||
|
||||
// Use case: initialize with default values
|
||||
defaultScores := u.Replicate(10, 100)
|
||||
fmt.Println(defaultScores)
|
||||
// [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]
|
||||
|
||||
// Use case: creating separators
|
||||
separator := u.Replicate(40, "-")
|
||||
fmt.Println(u.Reduce(separator, func(s, acc string) string { return acc + s }, ""))
|
||||
// ----------------------------------------
|
||||
}
|
||||
```
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
---
|
||||
title: "Scan"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Scan` is like Reduce but returns all intermediate accumulator values. Also known as prefix scan or cumulative fold. Useful for tracking running totals, running maximums, or other cumulative operations.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Running sum
|
||||
nums := []int{1, 2, 3, 4}
|
||||
add := func(acc, n int) int { return acc + n }
|
||||
fmt.Println(u.Scan(nums, 0, add)) // [1, 3, 6, 10]
|
||||
|
||||
// Running maximum
|
||||
values := []int{3, 1, 4, 1, 5, 9, 2}
|
||||
max := func(acc, n int) int {
|
||||
if n > acc {
|
||||
return n
|
||||
}
|
||||
return acc
|
||||
}
|
||||
fmt.Println(u.Scan(values, 0, max)) // [3, 3, 4, 4, 5, 9, 9]
|
||||
|
||||
// String concatenation
|
||||
words := []string{"hello", "world", "!"}
|
||||
concat := func(acc, s string) string { return acc + s }
|
||||
fmt.Println(u.Scan(words, "", concat)) // ["hello", "helloworld", "helloworld!"]
|
||||
}
|
||||
```
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
title: "Sliding"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Sliding` creates a sliding window view of the slice with the specified window size. Returns an empty slice if size is less than or equal to 0 or greater than the slice length. Useful for moving averages, n-grams, and pattern matching.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
fmt.Println(u.Sliding(nums, 3)) // [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
|
||||
|
||||
// Size 2
|
||||
fmt.Println(u.Sliding(nums, 2)) // [[1, 2], [2, 3], [3, 4], [4, 5]]
|
||||
|
||||
// N-grams for text
|
||||
words := []string{"the", "quick", "brown", "fox"}
|
||||
bigrams := u.Sliding(words, 2)
|
||||
fmt.Println(bigrams) // [["the", "quick"], ["quick", "brown"], ["brown", "fox"]]
|
||||
|
||||
// Moving average example
|
||||
data := []int{10, 20, 30, 40, 50}
|
||||
windows := u.Sliding(data, 3)
|
||||
for _, window := range windows {
|
||||
sum := 0
|
||||
for _, v := range window {
|
||||
sum += v
|
||||
}
|
||||
avg := sum / len(window)
|
||||
fmt.Printf("Window: %v, Average: %d\n", window, avg)
|
||||
}
|
||||
// Window: [10 20 30], Average: 20
|
||||
// Window: [20 30 40], Average: 30
|
||||
// Window: [30 40 50], Average: 40
|
||||
}
|
||||
```
|
||||
|
|
@ -9,13 +9,13 @@ date: 2022-03-21T13:50:29-04:00
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
fmt.Println(u.Sum(nums)) // 45
|
||||
fmt.Println(u.Sum(nums)) // 45
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
title: "TakeWhile"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`TakeWhile` returns elements from the beginning of the slice while the predicate returns true. It stops at the first element where the predicate returns false.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
lessThan5 := func(n int) bool { return n < 5 }
|
||||
fmt.Println(u.TakeWhile(nums, lessThan5)) // [1, 2, 3, 4]
|
||||
|
||||
words := []string{"apple", "banana", "cherry", "date"}
|
||||
shortWords := func(s string) bool { return len(s) < 6 }
|
||||
fmt.Println(u.TakeWhile(words, shortWords)) // ["apple"]
|
||||
}
|
||||
```
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
---
|
||||
title: "Tap"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Tap` applies a function to each element for side effects (like debugging or logging) and returns the original slice unchanged. Useful for debugging pipelines without breaking the flow.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Debugging a pipeline
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
|
||||
result := u.Tap(
|
||||
u.Map(
|
||||
u.Filter(nums, func(n int) bool { return n%2 == 0 }),
|
||||
func(n int) int { return n * 2 },
|
||||
),
|
||||
func(n int) {
|
||||
fmt.Printf("Debug: %d\n", n) // Prints each value
|
||||
},
|
||||
)
|
||||
|
||||
fmt.Println(result) // [4, 8]
|
||||
|
||||
// Counting elements that pass through
|
||||
count := 0
|
||||
filtered := u.Tap(
|
||||
u.Filter(nums, func(n int) bool { return n > 2 }),
|
||||
func(n int) { count++ },
|
||||
)
|
||||
fmt.Printf("Found %d elements: %v\n", count, filtered)
|
||||
// Found 3 elements: [3 4 5]
|
||||
|
||||
// Logging transformations
|
||||
data := []string{"hello", "world"}
|
||||
u.Tap(data, func(s string) {
|
||||
fmt.Printf("Processing: %s\n", s)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
---
|
||||
title: "Transpose"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Transpose` flips a matrix over its diagonal, swapping rows and columns. Returns an empty slice if the input is empty. Assumes all rows have the same length (uses the length of the first row).
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 2x3 matrix becomes 3x2 matrix
|
||||
matrix := [][]int{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
}
|
||||
transposed := u.Transpose(matrix)
|
||||
fmt.Println(transposed)
|
||||
// [[1, 4], [2, 5], [3, 6]]
|
||||
|
||||
// Square matrix
|
||||
square := [][]int{
|
||||
{1, 2},
|
||||
{3, 4},
|
||||
}
|
||||
fmt.Println(u.Transpose(square))
|
||||
// [[1, 3], [2, 4]]
|
||||
|
||||
// Use case: converting rows to columns for processing
|
||||
data := [][]string{
|
||||
{"Name", "Age", "City"},
|
||||
{"Alice", "30", "NYC"},
|
||||
{"Bob", "25", "LA"},
|
||||
}
|
||||
byColumn := u.Transpose(data)
|
||||
fmt.Println("Names:", byColumn[0]) // [Name Alice Bob]
|
||||
fmt.Println("Ages:", byColumn[1]) // [Age 30 25]
|
||||
fmt.Println("Cities:", byColumn[2]) // [City NYC LA]
|
||||
}
|
||||
```
|
||||
|
|
@ -9,13 +9,13 @@ date: 2022-04-12T17:18:04-04:00
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nums := []int{1, 4, 2, 5, 3, 1, 5, 2, 8, 9}
|
||||
|
||||
fmt.Println(u.Unique(nums)) // 1, 4, 2, 5, 3, 8, 9
|
||||
nums := []int{1, 4, 2, 5, 3, 1, 5, 2, 8, 9}
|
||||
|
||||
fmt.Println(u.Unique(nums)) // 1, 4, 2, 5, 3, 8, 9
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
title: "UniqueBy"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
`UniqueBy` returns a duplicate-free version of the slice using a key selector.
|
||||
Order is preserved; the first occurrence of each key is kept.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
type User struct{ ID int; Email string }
|
||||
|
||||
func main() {
|
||||
users := []User{{1, "a@x"}, {2, "b@x"}, {3, "a@x"}}
|
||||
fmt.Println(u.UniqueBy(users, func(u User) string { return u.Email }))
|
||||
// [{1 a@x} {2 b@x}]
|
||||
}
|
||||
```
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
title: "UniqueInPlace"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
`UniqueInPlace` removes duplicates from a slice in place while preserving order.
|
||||
Returns the shortened slice.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
xs := []int{1,4,2,5,3,1,5,2}
|
||||
fmt.Println(u.UniqueInPlace(xs)) // [1 4 2 5 3]
|
||||
}
|
||||
```
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
title: "Unzip"
|
||||
date: 2025-01-16T00:00:00-00:00
|
||||
---
|
||||
|
||||
`Unzip` splits a slice of tuples into two separate slices. The inverse operation of Zip. Useful for separating paired data.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Basic usage
|
||||
pairs := []u.Tuple[int, string]{
|
||||
{Left: 1, Right: "a"},
|
||||
{Left: 2, Right: "b"},
|
||||
{Left: 3, Right: "c"},
|
||||
}
|
||||
|
||||
nums, letters := u.Unzip(pairs)
|
||||
fmt.Println(nums) // [1, 2, 3]
|
||||
fmt.Println(letters) // ["a", "b", "c"]
|
||||
|
||||
// Use case: separating keys and values
|
||||
keyValuePairs := []u.Tuple[string, int]{
|
||||
{Left: "apple", Right: 5},
|
||||
{Left: "banana", Right: 3},
|
||||
{Left: "cherry", Right: 8},
|
||||
}
|
||||
|
||||
items, counts := u.Unzip(keyValuePairs)
|
||||
fmt.Println("Items:", items) // Items: [apple banana cherry]
|
||||
fmt.Println("Counts:", counts) // Counts: [5 3 8]
|
||||
|
||||
// Empty slice
|
||||
emptyNums, emptyStrs := u.Unzip([]u.Tuple[int, string]{})
|
||||
fmt.Println(emptyNums, emptyStrs) // [] []
|
||||
}
|
||||
```
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: "Map Helpers"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
Utilities for Go maps provided by the `maps` subpackage.
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
title: "Keys"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
`maps.Keys` returns the keys of a map in unspecified order.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
m "github.com/rjNemo/underscore/maps"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(m.Keys(map[int]string{1:"a",2:"b"})) // e.g., [2 1]
|
||||
}
|
||||
```
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
title: "Values"
|
||||
date: 2025-09-01T00:00:00-00:00
|
||||
---
|
||||
|
||||
`maps.Values` returns the values of a map in unspecified order.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
m "github.com/rjNemo/underscore/maps"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(m.Values(map[int]string{1:"a",2:"b"})) // e.g., ["b" "a"]
|
||||
}
|
||||
```
|
||||
|
|
@ -3,30 +3,29 @@ title: "NewPipe"
|
|||
date: 2021-12-31T13:11:41-04:00
|
||||
---
|
||||
|
||||
Calling `NewPipe` will cause all future method calls to return wrapped objects.
|
||||
When you've finished the computation, call `Value` to retrieve the final value.
|
||||
|
||||
Methods not returning a collection such as `Reduce`, `All`, `Any`, will break the
|
||||
chain and return `Value` instantly.
|
||||
Calling `NewPipe` will cause all future method calls to return wrapped objects. When you've finished the computation,
|
||||
call `Value` to retrieve the final value.
|
||||
|
||||
Methods not returning a collection such as `Reduce`, `All`, `Any`, will break the chain and return `Value` instantly.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
sum := u.NewPipe([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}).
|
||||
// filter even numbers from the slice
|
||||
Filter(func(n int) bool { return n%2 == 0 }).
|
||||
// square every number in the slice
|
||||
Map(func(n int) int { return n * n }).
|
||||
// reduce to the sum
|
||||
Reduce(func(n, acc int) int { return n + acc }, 0)
|
||||
sum := u.NewPipe([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}).
|
||||
// filter even numbers from the slice
|
||||
Filter(func(n int) bool { return n%2 == 0 }).
|
||||
// square every number in the slice
|
||||
Map(func(n int) int { return n * n }).
|
||||
// reduce to the sum
|
||||
Reduce(func(n, acc int) int { return n + acc }, 0)
|
||||
|
||||
fmt.Println(sum) // 120
|
||||
fmt.Println(sum) // 120
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
|
|
@ -11,40 +11,38 @@ Install the library using
|
|||
go get github.com/rjNemo/underscore
|
||||
```
|
||||
|
||||
Please check out the [examples](https://github.com/rjNemo/underscore/tree/main/examples)
|
||||
to see how to use the library.
|
||||
Please check out the [examples](https://github.com/rjNemo/underscore/tree/main/examples) to see how to use the library.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
"fmt"
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
// filter even numbers from the slice
|
||||
evens := u.Filter(numbers, func(n int) bool { return n%2 == 0 })
|
||||
// square every number in the slice
|
||||
squares := u.Map(evens, func(n int) int { return n * n })
|
||||
// reduce to the sum
|
||||
res := u.Reduce(squares, func(n, acc int) int { return n + acc }, 0)
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
// filter even numbers from the slice
|
||||
evens := u.Filter(numbers, func(n int) bool { return n%2 == 0 })
|
||||
// square every number in the slice
|
||||
squares := u.Map(evens, func(n int) int { return n * n })
|
||||
// reduce to the sum
|
||||
res := u.Reduce(squares, func(n, acc int) int { return n + acc }, 0)
|
||||
|
||||
fmt.Println(res) // 120
|
||||
fmt.Println(res) // 120
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
These instructions will get you a copy of the project up and running on your local
|
||||
machine for development and testing purposes.
|
||||
These instructions will get you a copy of the project up and running on your local machine for development and testing
|
||||
purposes.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You need at least `go1.18` for development. The project is shipped with
|
||||
a [Dockerfile](https://github.com/rjNemo/underscore/tree/main/Dockerfile) based
|
||||
on `go1.18`.
|
||||
a [Dockerfile](https://github.com/rjNemo/underscore/tree/main/Dockerfile) based on `go1.18`.
|
||||
|
||||
If you prefer local development, navigate to the [official
|
||||
download page](https://go.dev/dl/) and install version `1.18` or beyond.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{ if hugo.IsMultilingual }}
|
||||
{{ if .Site.IsMultiLingual }}
|
||||
<span class="gdoc-language">
|
||||
<ul class="gdoc-language__selector" role="button" aria-pressed="false" tabindex="0">
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,6 @@
|
|||
{{- end }}
|
||||
|
||||
{{- /* Facebook Page Admin ID for Domain Insights */}}
|
||||
{{- with .Site.Params.facebook.adminID }}
|
||||
{{- with .Site.Social.facebook_admin }}
|
||||
<meta property="fb:admins" content="{{ . }}" />
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@
|
|||
{{- with partial "utils/description" . }}
|
||||
<meta name="twitter:description" content="{{ . | plainify | htmlUnescape | chomp }}" />
|
||||
{{- end }}
|
||||
{{- with .Site.Params.twitter -}}
|
||||
{{- with .Site.Social.twitter -}}
|
||||
<meta name="twitter:site" content="@{{ . }}" />
|
||||
{{- end }}
|
||||
|
|
|
|||
20
drop.go
20
drop.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
33
drop_test.go
33
drop_test.go
|
|
@ -9,34 +9,9 @@ import (
|
|||
)
|
||||
|
||||
func TestDrop(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
want := []int{3, 4, 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))
|
||||
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, 3))
|
||||
}
|
||||
|
|
|
|||
15
dropwhile.go
15
dropwhile.go
|
|
@ -1,15 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// DropWhile drops elements from the beginning of the slice while the predicate returns true.
|
||||
// It returns the remaining elements starting from the first element where the predicate returns false.
|
||||
func DropWhile[T any](values []T, predicate func(T) bool) []T {
|
||||
for i, v := range values {
|
||||
if !predicate(v) {
|
||||
res := make([]T, len(values)-i)
|
||||
copy(res, values[i:])
|
||||
return res
|
||||
}
|
||||
}
|
||||
// All elements satisfy predicate, return empty slice
|
||||
return []T{}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestDropWhile(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
result := u.DropWhile(nums, func(n int) bool { return n < 5 })
|
||||
assert.Equal(t, []int{5, 6, 7, 8, 9}, result)
|
||||
}
|
||||
|
||||
func TestDropWhileEmpty(t *testing.T) {
|
||||
result := u.DropWhile([]int{}, func(n int) bool { return n < 5 })
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestDropWhileNoneMatch(t *testing.T) {
|
||||
nums := []int{5, 6, 7, 8, 9}
|
||||
result := u.DropWhile(nums, func(n int) bool { return n < 5 })
|
||||
assert.Equal(t, []int{5, 6, 7, 8, 9}, result)
|
||||
}
|
||||
|
||||
func TestDropWhileAllMatch(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4}
|
||||
result := u.DropWhile(nums, func(n int) bool { return n < 10 })
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestDropWhileSingleElement(t *testing.T) {
|
||||
result := u.DropWhile([]int{5}, func(n int) bool { return n < 10 })
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestDropWhileStrings(t *testing.T) {
|
||||
words := []string{"apple", "banana", "cherry", "date"}
|
||||
result := u.DropWhile(words, func(s string) bool { return len(s) < 6 })
|
||||
assert.Equal(t, []string{"banana", "cherry", "date"}, result)
|
||||
}
|
||||
|
||||
func BenchmarkDropWhile(b *testing.B) {
|
||||
nums := make([]int, 1000)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.DropWhile(nums, func(n int) bool { return n < 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -2,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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ package underscore_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
|
|
|
|||
33
first.go
33
first.go
|
|
@ -1,33 +0,0 @@
|
|||
package underscore
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrEmptySlice is returned when trying to get the first element of an empty slice
|
||||
var ErrEmptySlice = errors.New("underscore: empty slice")
|
||||
|
||||
// First returns the first element of the slice.
|
||||
// Returns an error if the slice is empty.
|
||||
func First[T any](values []T) (T, error) {
|
||||
var zero T
|
||||
if len(values) == 0 {
|
||||
return zero, ErrEmptySlice
|
||||
}
|
||||
return values[0], nil
|
||||
}
|
||||
|
||||
// FirstN returns the first n elements of the slice.
|
||||
// If n is greater than the slice length, returns the entire slice.
|
||||
// If n is less than or equal to 0, returns an empty slice.
|
||||
func FirstN[T any](values []T, n int) []T {
|
||||
if n <= 0 {
|
||||
return []T{}
|
||||
}
|
||||
if n >= len(values) {
|
||||
res := make([]T, len(values))
|
||||
copy(res, values)
|
||||
return res
|
||||
}
|
||||
res := make([]T, n)
|
||||
copy(res, values[:n])
|
||||
return res
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestFirst(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
result, err := u.First(nums)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, result)
|
||||
}
|
||||
|
||||
func TestFirstEmpty(t *testing.T) {
|
||||
_, err := u.First([]int{})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, u.ErrEmptySlice))
|
||||
}
|
||||
|
||||
func TestFirstSingleElement(t *testing.T) {
|
||||
result, err := u.First([]int{42})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestFirstStrings(t *testing.T) {
|
||||
words := []string{"hello", "world"}
|
||||
result, err := u.First(words)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", result)
|
||||
}
|
||||
|
||||
func TestFirstN(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
result := u.FirstN(nums, 3)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
}
|
||||
|
||||
func TestFirstNEmpty(t *testing.T) {
|
||||
result := u.FirstN([]int{}, 3)
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestFirstNZero(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.FirstN(nums, 0)
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestFirstNNegative(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.FirstN(nums, -5)
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestFirstNGreaterThanLength(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.FirstN(nums, 10)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
}
|
||||
|
||||
func TestFirstNSingleElement(t *testing.T) {
|
||||
result := u.FirstN([]int{42}, 1)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
}
|
||||
|
||||
func TestFirstNAll(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.FirstN(nums, 3)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
}
|
||||
|
||||
func BenchmarkFirst(b *testing.B) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = u.First(nums)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFirstN(b *testing.B) {
|
||||
nums := make([]int, 1000)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.FirstN(nums, 100)
|
||||
}
|
||||
}
|
||||
12
flatmap.go
12
flatmap.go
|
|
@ -1,12 +0,0 @@
|
|||
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)
|
||||
for _, v := range values {
|
||||
vs := mapper(v)
|
||||
res = append(res, vs...)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestFlatmap(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4}
|
||||
transform := func(n int) []int { return []int{(n - 1) * n, (n) * n} }
|
||||
want := []int{0, 1, 2, 4, 6, 9, 12, 16}
|
||||
|
||||
assert.Equal(t, want, u.Flatmap(nums, transform))
|
||||
}
|
||||
|
||||
func BenchmarkFlatmap(b *testing.B) {
|
||||
data := make([]int, 100)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
mapper := func(n int) []int { return []int{n, n * 2, n * 3} }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.Flatmap(data, mapper)
|
||||
}
|
||||
}
|
||||
14
foldright.go
14
foldright.go
|
|
@ -1,14 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// FoldRight is like Reduce but processes elements from right to left.
|
||||
// Also known as foldr in Haskell.
|
||||
//
|
||||
// Example: FoldRight([]int{1,2,3}, 0, func(n, acc int) int { return n - acc })
|
||||
//
|
||||
// → 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2
|
||||
func FoldRight[T, P any](values []T, acc P, fn func(T, P) P) P {
|
||||
for i := len(values) - 1; i >= 0; i-- {
|
||||
acc = fn(values[i], acc)
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestFoldRight(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4}
|
||||
result := u.FoldRight(nums, 0, func(n, acc int) int { return n + acc })
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestFoldRightEmpty(t *testing.T) {
|
||||
result := u.FoldRight([]int{}, 42, func(n, acc int) int { return n + acc })
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestFoldRightSingleElement(t *testing.T) {
|
||||
result := u.FoldRight([]int{5}, 0, func(n, acc int) int { return n + acc })
|
||||
assert.Equal(t, 5, result)
|
||||
}
|
||||
|
||||
func TestFoldRightSubtraction(t *testing.T) {
|
||||
// FoldRight: 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.FoldRight(nums, 0, func(n, acc int) int { return n - acc })
|
||||
assert.Equal(t, 2, result)
|
||||
}
|
||||
|
||||
func TestFoldRightDivision(t *testing.T) {
|
||||
// FoldRight with float: 2.0 / (4.0 / (8.0 / 1.0)) = 2.0 / (4.0 / 8.0) = 2.0 / 0.5 = 4.0
|
||||
nums := []float64{2.0, 4.0, 8.0}
|
||||
result := u.FoldRight(nums, 1.0, func(n, acc float64) float64 { return n / acc })
|
||||
assert.Equal(t, 4.0, result)
|
||||
}
|
||||
|
||||
func TestFoldRightStrings(t *testing.T) {
|
||||
words := []string{"a", "b", "c"}
|
||||
result := u.FoldRight(words, "", func(s, acc string) string { return s + acc })
|
||||
assert.Equal(t, "abc", result)
|
||||
}
|
||||
|
||||
func TestFoldRightVsReduce(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
|
||||
// Reduce (left fold): (0 - 1) - 2 - 3 = -6
|
||||
reduceResult := u.Reduce(nums, func(n, acc int) int { return acc - n }, 0)
|
||||
assert.Equal(t, -6, reduceResult)
|
||||
|
||||
// FoldRight: 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2
|
||||
foldRightResult := u.FoldRight(nums, 0, func(n, acc int) int { return n - acc })
|
||||
assert.Equal(t, 2, foldRightResult)
|
||||
|
||||
// They should be different for non-associative operations
|
||||
assert.NotEqual(t, reduceResult, foldRightResult)
|
||||
}
|
||||
|
||||
func TestFoldRightBuildList(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.FoldRight(nums, []int{}, func(n int, acc []int) []int {
|
||||
return append([]int{n}, acc...)
|
||||
})
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
}
|
||||
|
||||
func BenchmarkFoldRight(b *testing.B) {
|
||||
nums := make([]int, 1000)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.FoldRight(nums, 0, func(n, acc int) int { return n + acc })
|
||||
}
|
||||
}
|
||||
9
go.mod
9
go.mod
|
|
@ -1,11 +1,14 @@
|
|||
module github.com/rjNemo/underscore
|
||||
|
||||
go 1.24.2
|
||||
go 1.18
|
||||
|
||||
require github.com/stretchr/testify v1.8.4
|
||||
require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/exp v0.0.0-20220314205449-43aec2f8a4e7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
|
|
|||
13
go.sum
13
go.sum
|
|
@ -1,10 +1,15 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/exp v0.0.0-20220314205449-43aec2f8a4e7 h1:jynE66seADJbyWMUdeOyVTvPtBZt7L6LJHupGwxPZRM=
|
||||
golang.org/x/exp v0.0.0-20220314205449-43aec2f8a4e7/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
18
init.go
18
init.go
|
|
@ -1,18 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Init returns all elements except the last one, and the last element separately.
|
||||
// Returns an empty slice and zero value if the input slice is empty.
|
||||
// Also known as "uncons from the right" or "snoc" inverse.
|
||||
func Init[T any](values []T) ([]T, T) {
|
||||
var last T
|
||||
if len(values) == 0 {
|
||||
return []T{}, last
|
||||
}
|
||||
if len(values) == 1 {
|
||||
return []T{}, values[0]
|
||||
}
|
||||
|
||||
res := make([]T, len(values)-1)
|
||||
copy(res, values[:len(values)-1])
|
||||
return res, values[len(values)-1]
|
||||
}
|
||||
65
init_test.go
65
init_test.go
|
|
@ -1,65 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
init, last := u.Init(nums)
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, init)
|
||||
assert.Equal(t, 5, last)
|
||||
}
|
||||
|
||||
func TestInitEmpty(t *testing.T) {
|
||||
init, last := u.Init([]int{})
|
||||
assert.Equal(t, []int{}, init)
|
||||
assert.Equal(t, 0, last)
|
||||
}
|
||||
|
||||
func TestInitSingleElement(t *testing.T) {
|
||||
init, last := u.Init([]int{42})
|
||||
assert.Equal(t, []int{}, init)
|
||||
assert.Equal(t, 42, last)
|
||||
}
|
||||
|
||||
func TestInitTwoElements(t *testing.T) {
|
||||
init, last := u.Init([]int{1, 2})
|
||||
assert.Equal(t, []int{1}, init)
|
||||
assert.Equal(t, 2, last)
|
||||
}
|
||||
|
||||
func TestInitStrings(t *testing.T) {
|
||||
words := []string{"hello", "world", "!"}
|
||||
init, last := u.Init(words)
|
||||
assert.Equal(t, []string{"hello", "world"}, init)
|
||||
assert.Equal(t, "!", last)
|
||||
}
|
||||
|
||||
func TestInitDoesNotMutate(t *testing.T) {
|
||||
original := []int{1, 2, 3, 4, 5}
|
||||
init, last := u.Init(original)
|
||||
|
||||
// Modify returned slice
|
||||
init[0] = 999
|
||||
|
||||
// Original should be unchanged
|
||||
assert.Equal(t, 1, original[0])
|
||||
assert.Equal(t, 5, last)
|
||||
}
|
||||
|
||||
func BenchmarkInit(b *testing.B) {
|
||||
nums := make([]int, 1000)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.Init(nums)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@ package underscore_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIntersection(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Intersperse inserts a separator between each element of the slice.
|
||||
// Returns an empty slice if the input is empty.
|
||||
// Returns the original element if the input has only one element.
|
||||
//
|
||||
// Example: Intersperse([]int{1,2,3}, 0) → [1, 0, 2, 0, 3]
|
||||
func Intersperse[T any](values []T, separator T) []T {
|
||||
if len(values) == 0 {
|
||||
return []T{}
|
||||
}
|
||||
if len(values) == 1 {
|
||||
return []T{values[0]}
|
||||
}
|
||||
|
||||
// Result will have len(values) + (len(values)-1) elements
|
||||
res := make([]T, 0, len(values)*2-1)
|
||||
res = append(res, values[0])
|
||||
for i := 1; i < len(values); i++ {
|
||||
res = append(res, separator, values[i])
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestIntersperse(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
result := u.Intersperse(nums, 0)
|
||||
assert.Equal(t, []int{1, 0, 2, 0, 3, 0, 4, 0, 5}, result)
|
||||
}
|
||||
|
||||
func TestIntersperseEmpty(t *testing.T) {
|
||||
result := u.Intersperse([]int{}, 0)
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestIntersperseSingleElement(t *testing.T) {
|
||||
result := u.Intersperse([]int{42}, 0)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
}
|
||||
|
||||
func TestIntersperseTwoElements(t *testing.T) {
|
||||
result := u.Intersperse([]int{1, 2}, 0)
|
||||
assert.Equal(t, []int{1, 0, 2}, result)
|
||||
}
|
||||
|
||||
func TestIntersperseStrings(t *testing.T) {
|
||||
words := []string{"hello", "world", "!"}
|
||||
result := u.Intersperse(words, ",")
|
||||
assert.Equal(t, []string{"hello", ",", "world", ",", "!"}, result)
|
||||
}
|
||||
|
||||
func TestIntersperseComma(t *testing.T) {
|
||||
words := []string{"apple", "banana", "cherry"}
|
||||
result := u.Intersperse(words, ",")
|
||||
assert.Equal(t, []string{"apple", ",", "banana", ",", "cherry"}, result)
|
||||
}
|
||||
|
||||
func TestIntersperseNegativeNumber(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.Intersperse(nums, -1)
|
||||
assert.Equal(t, []int{1, -1, 2, -1, 3}, result)
|
||||
}
|
||||
|
||||
func BenchmarkIntersperse(b *testing.B) {
|
||||
nums := make([]int, 100)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.Intersperse(nums, 0)
|
||||
}
|
||||
}
|
||||
38
join.go
38
join.go
|
|
@ -1,38 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Join joins two slices together and returns a Tuple of [T, []P], the selectors allow you to pick the
|
||||
// keys you want to use from your struct's to join the sets together
|
||||
func Join[T, P any, S comparable](
|
||||
left []T,
|
||||
right []P,
|
||||
leftSelector func(T) S,
|
||||
rightSelector func(P) S,
|
||||
) []Tuple[T, []P] {
|
||||
results := make([]Tuple[T, []P], 0, len(left))
|
||||
for _, l := range left {
|
||||
matches := Filter(right, func(r P) bool { return leftSelector(l) == rightSelector(r) })
|
||||
tuple := Tuple[T, []P]{Left: l, Right: matches}
|
||||
results = append(results, tuple)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// JoinProject joins two slices together and returns a []O where O is defined by the output
|
||||
// of your projection function
|
||||
// The selectors allow you to pick the keys from your structure to use as the join keys
|
||||
// While the projection functions allows you to reformat joined datasets
|
||||
// (Tuple of [T, []P]) into your own struct or type
|
||||
func JoinProject[L, R, O any, S comparable](
|
||||
left []L,
|
||||
right []R,
|
||||
leftSelector func(L) S,
|
||||
rightSelector func(R) S,
|
||||
projection func(Tuple[L, []R]) O,
|
||||
) (results []O) {
|
||||
for _, x := range Join(left, right, leftSelector, rightSelector) {
|
||||
results = append(results, projection(x))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
48
join_test.go
48
join_test.go
|
|
@ -1,48 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
var (
|
||||
zero = u.Tuple[int, string]{Left: 0, Right: "Zero"}
|
||||
one = u.Tuple[int, string]{Left: 1, Right: "One"}
|
||||
two = u.Tuple[int, string]{Left: 2, Right: "Two"}
|
||||
three = u.Tuple[int, string]{Left: 3, Right: "Three"}
|
||||
)
|
||||
|
||||
func Test_Join_Can_Join_Two_Slices_Together(t *testing.T) {
|
||||
left := []u.Tuple[int, string]{zero, one, two, three}
|
||||
right := []u.Tuple[int, string]{one, three, two, three, two, three}
|
||||
|
||||
selector := func(x u.Tuple[int, string]) int { return x.Left }
|
||||
|
||||
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: 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}},
|
||||
}
|
||||
|
||||
assert.Equal(t, want, joined)
|
||||
}
|
||||
|
||||
func Test_Join_Can_Join_and_Project_Two_Slices_Together(t *testing.T) {
|
||||
left := []u.Tuple[int, string]{zero, one, two, three}
|
||||
right := []u.Tuple[int, string]{one, three, two, three, two, three}
|
||||
|
||||
selector := func(x u.Tuple[int, string]) int { return x.Left }
|
||||
project := func(x u.Tuple[u.Tuple[int, string], []u.Tuple[int, string]]) int {
|
||||
return len(x.Right) // projecting to a could of how many
|
||||
}
|
||||
|
||||
joined := u.JoinProject(left, right, selector, selector, project)
|
||||
want := []int{0, 1, 2, 3}
|
||||
|
||||
assert.Equal(t, want, joined)
|
||||
}
|
||||
9
last.go
9
last.go
|
|
@ -1,10 +1,7 @@
|
|||
package underscore
|
||||
|
||||
// Last returns the last element of the slice.
|
||||
// Panics if the slice is empty.
|
||||
// Last returns the last element of the slice
|
||||
func Last[T any](values []T) T {
|
||||
if len(values) == 0 {
|
||||
panic("underscore.Last: empty slice")
|
||||
}
|
||||
return values[len(values)-1]
|
||||
n := len(values)
|
||||
return values[n-1]
|
||||
}
|
||||
|
|
|
|||
10
last_test.go
10
last_test.go
|
|
@ -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}))
|
||||
}
|
||||
|
|
|
|||
33
map_test.go
33
map_test.go
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package maps
|
||||
|
||||
// Keys returns the keys of the provided map in unspecified order.
|
||||
func Keys[K comparable, V any](m map[K]V) []K {
|
||||
ks := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
return ks
|
||||
}
|
||||
|
||||
// Values returns the values of the provided map in unspecified order.
|
||||
func Values[K comparable, V any](m map[K]V) []V {
|
||||
vs := make([]V, 0, len(m))
|
||||
for _, v := range m {
|
||||
vs = append(vs, v)
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package maps_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
m "github.com/rjNemo/underscore/maps"
|
||||
)
|
||||
|
||||
func TestKeysValues(t *testing.T) {
|
||||
in := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
ks := m.Keys(in)
|
||||
vs := m.Values(in)
|
||||
|
||||
// Order is unspecified; verify content and lengths.
|
||||
assert.Len(t, ks, 3)
|
||||
assert.ElementsMatch(t, []int{1, 2, 3}, ks)
|
||||
|
||||
assert.Len(t, vs, 3)
|
||||
assert.ElementsMatch(t, []string{"a", "b", "c"}, vs)
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package maps
|
||||
|
||||
import "maps"
|
||||
|
||||
type M[K comparable, V any] map[K]V
|
||||
|
||||
// Map produces a new slice of values by mapping each value in the slice through
|
||||
|
|
@ -10,7 +8,9 @@ func Map[K, Q comparable, V, W any](m M[K, V], f func(K, V) M[Q, W]) M[Q, W] {
|
|||
res := make(M[Q, W], len(m))
|
||||
for k, v := range m {
|
||||
mm := f(k, v)
|
||||
maps.Copy(res, mm)
|
||||
for k2, v2 := range mm {
|
||||
res[k2] = v2
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@ func TestMap(t *testing.T) {
|
|||
"alice": false,
|
||||
"bob": false,
|
||||
"clara": false,
|
||||
"david": true,
|
||||
}
|
||||
"david": true}
|
||||
assert.Equal(t, want, m.Map(scores, hasWon))
|
||||
}
|
||||
|
||||
|
|
|
|||
8
max.go
8
max.go
|
|
@ -1,15 +1,11 @@
|
|||
package underscore
|
||||
|
||||
import "cmp"
|
||||
import "golang.org/x/exp/constraints"
|
||||
|
||||
// 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")
|
||||
}
|
||||
func Max[T constraints.Ordered](values []T) T {
|
||||
max := values[0]
|
||||
for _, v := range values {
|
||||
if v > max {
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
8
min.go
8
min.go
|
|
@ -1,15 +1,11 @@
|
|||
package underscore
|
||||
|
||||
import "cmp"
|
||||
import "golang.org/x/exp/constraints"
|
||||
|
||||
// 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")
|
||||
}
|
||||
func Min[T constraints.Ordered](values []T) T {
|
||||
min := values[0]
|
||||
for _, v := range values {
|
||||
if v < min {
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
21
orderBy.go
21
orderBy.go
|
|
@ -1,21 +0,0 @@
|
|||
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
|
||||
}
|
||||
if predicate(b, a) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func Test_OrderBy_Asc(t *testing.T) {
|
||||
set := u.Range(5, 0)
|
||||
want := u.Range(0, 5)
|
||||
|
||||
result := u.OrderBy(set, func(left int, right int) bool {
|
||||
return left > right
|
||||
})
|
||||
|
||||
assert.Equal(t, want, result)
|
||||
}
|
||||
|
||||
func Test_OrderBy_Desc(t *testing.T) {
|
||||
set := u.Range(0, 5)
|
||||
want := u.Range(5, 0)
|
||||
|
||||
result := u.OrderBy(set, func(left int, right int) bool {
|
||||
return left < right
|
||||
})
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
package underscore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ParallelFilter filters values using a context-aware predicate concurrently and preserves input order.
|
||||
// If workers <= 0, it defaults to GOMAXPROCS. On error, cancels work and returns nil with the error.
|
||||
func ParallelFilter[T any](ctx context.Context, values []T, workers int, fn func(context.Context, T) (bool, error)) ([]T, error) {
|
||||
if workers <= 0 {
|
||||
workers = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
type task struct {
|
||||
idx int
|
||||
val T
|
||||
}
|
||||
|
||||
keeps := make([]bool, len(values))
|
||||
tasks := make(chan task)
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var once sync.Once
|
||||
var firstErr error
|
||||
|
||||
worker := func() {
|
||||
defer wg.Done()
|
||||
for t := range tasks {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
keep, err := fn(ctx, t.val)
|
||||
if err != nil {
|
||||
once.Do(func() {
|
||||
firstErr = err
|
||||
cancel()
|
||||
})
|
||||
continue
|
||||
}
|
||||
keeps[t.idx] = keep
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go worker()
|
||||
}
|
||||
|
||||
OUTER:
|
||||
for i, v := range values {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break OUTER
|
||||
default:
|
||||
tasks <- task{idx: i, val: v}
|
||||
}
|
||||
}
|
||||
close(tasks)
|
||||
wg.Wait()
|
||||
|
||||
if firstErr != nil {
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
// Build result preserving order
|
||||
// Pre-count capacity to avoid re-allocations
|
||||
count := 0
|
||||
for _, k := range keeps {
|
||||
if k {
|
||||
count++
|
||||
}
|
||||
}
|
||||
res := make([]T, 0, count)
|
||||
for i, k := range keeps {
|
||||
if k {
|
||||
res = append(res, values[i])
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue