mirror of
https://github.com/rjNemo/underscore
synced 2026-06-07 02:56:42 +00:00
Compare commits
No commits in common. "main" and "v0.9.0" have entirely different histories.
45 changed files with 1199 additions and 1710 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -60,7 +60,3 @@ Temporary Items
|
|||
docs/public
|
||||
.trivycache/
|
||||
.vscode/launch.json
|
||||
.claude
|
||||
AGENTS.md
|
||||
bench*txt
|
||||
ACTION_PLAN.md
|
||||
|
|
|
|||
1189
ACTION_PLAN.md
Normal file
1189
ACTION_PLAN.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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"]
|
||||
}
|
||||
```
|
||||
|
|
@ -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,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,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
|
||||
}
|
||||
```
|
||||
|
|
@ -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,26 +3,19 @@ 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
|
||||
|
||||
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.Last(nums)) // 5
|
||||
nums := []int{1, 9, 2, 8, 3, 7, 4, 6, 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
|
||||
fmt.Println(u.Last(nums)) // 5
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
@ -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]
|
||||
}
|
||||
```
|
||||
|
|
@ -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) // [] []
|
||||
}
|
||||
```
|
||||
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 })
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ func BenchmarkParallelMap(b *testing.B) {
|
|||
b.Run(fmt.Sprintf("workers=%d", workers), func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = u.ParallelMap(ctx, data, workers, func(_ context.Context, n int) (int, error) {
|
||||
u.ParallelMap(ctx, data, workers, func(_ context.Context, n int) (int, error) {
|
||||
return n * 2, nil
|
||||
})
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ func BenchmarkMapVsParallelMap(b *testing.B) {
|
|||
|
||||
b.Run("ParallelMap", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = u.ParallelMap(ctx, data, 0, func(_ context.Context, n int) (int, error) {
|
||||
u.ParallelMap(ctx, data, 0, func(_ context.Context, n int) (int, error) {
|
||||
return n * 2, nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
package underscore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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: Order of operations is not guaranteed, so use only with associative/commutative operations.
|
||||
func ParallelReduce[T, P any](ctx context.Context, values []T, workers int, fn func(context.Context, T, P) (P, error), acc P) (P, error) {
|
||||
if workers <= 0 {
|
||||
workers = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
type task struct {
|
||||
idx int
|
||||
val T
|
||||
}
|
||||
|
||||
tasks := make(chan task)
|
||||
results := make(chan P, len(values))
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var once sync.Once
|
||||
var firstErr error
|
||||
|
||||
// Workers
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for t := range tasks {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
result, err := fn(ctx, t.val, acc)
|
||||
if err != nil {
|
||||
once.Do(func() {
|
||||
firstErr = err
|
||||
cancel()
|
||||
})
|
||||
return
|
||||
}
|
||||
results <- result
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Send tasks
|
||||
go func() {
|
||||
for i, v := range values {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(tasks)
|
||||
return
|
||||
default:
|
||||
tasks <- task{idx: i, val: v}
|
||||
}
|
||||
}
|
||||
close(tasks)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
if firstErr != nil {
|
||||
return acc, firstErr
|
||||
}
|
||||
|
||||
// Combine results
|
||||
for result := range results {
|
||||
// This is a simplified combination - in practice, you'd need a combiner function
|
||||
acc = result
|
||||
}
|
||||
|
||||
return acc, nil
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestParallelReduce(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
ctx := context.Background()
|
||||
|
||||
// Note: This is a simplified test - ParallelReduce needs work for proper reduction
|
||||
result, err := u.ParallelReduce(ctx, nums, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
assert.NoError(t, err)
|
||||
// Result may vary due to parallel execution
|
||||
assert.Greater(t, result, 0)
|
||||
}
|
||||
|
||||
func TestParallelReduceEmpty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
result, err := u.ParallelReduce(ctx, []int{}, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
return n + acc, nil
|
||||
}, 42)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestParallelReduceDefaultWorkers(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with workers <= 0 to use GOMAXPROCS
|
||||
result, err := u.ParallelReduce(ctx, nums, 0, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, result, 0)
|
||||
}
|
||||
|
||||
func TestParallelReduceNegativeWorkers(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
ctx := context.Background()
|
||||
|
||||
// Negative workers should default to GOMAXPROCS
|
||||
result, err := u.ParallelReduce(ctx, nums, -1, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, result, 0)
|
||||
}
|
||||
|
||||
func TestParallelReduceError(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
ctx := context.Background()
|
||||
|
||||
expectedErr := errors.New("processing error")
|
||||
_, err := u.ParallelReduce(ctx, nums, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
if n == 3 {
|
||||
return 0, expectedErr
|
||||
}
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
}
|
||||
|
||||
func TestParallelReduceContextCancellation(t *testing.T) {
|
||||
nums := make([]int, 100)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Cancel after a short delay
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
_, err := u.ParallelReduce(ctx, nums, 4, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
// Slow processing to allow cancellation
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
return n + acc, nil
|
||||
}
|
||||
}, 0)
|
||||
|
||||
// Should either complete or get cancelled
|
||||
if err != nil {
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallelReduceContextTimeout(t *testing.T) {
|
||||
nums := make([]int, 20)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
_, err := u.ParallelReduce(ctx, nums, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
// Simulate slow work
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if ctx.Err() != nil {
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
// Should timeout
|
||||
if err != nil {
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallelReduceSingleElement(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
result, err := u.ParallelReduce(ctx, []int{42}, 2, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, result, 0)
|
||||
}
|
||||
|
||||
func TestParallelReduceManyWorkers(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
ctx := context.Background()
|
||||
|
||||
// More workers than elements
|
||||
result, err := u.ParallelReduce(ctx, nums, 10, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, result, 0)
|
||||
}
|
||||
|
||||
func BenchmarkParallelReduce(b *testing.B) {
|
||||
nums := make([]int, 100)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = u.ParallelReduce(ctx, nums, 4, func(ctx context.Context, n int, acc int) (int, error) {
|
||||
return n + acc, nil
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
17
replicate.go
17
replicate.go
|
|
@ -1,17 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Replicate creates a slice containing count copies of value.
|
||||
// Returns an empty slice if count is less than or equal to 0.
|
||||
//
|
||||
// Example: Replicate(3, "hello") → ["hello", "hello", "hello"]
|
||||
func Replicate[T any](count int, value T) []T {
|
||||
if count <= 0 {
|
||||
return []T{}
|
||||
}
|
||||
|
||||
res := make([]T, count)
|
||||
for i := range res {
|
||||
res[i] = value
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestReplicate(t *testing.T) {
|
||||
result := u.Replicate(3, "hello")
|
||||
assert.Equal(t, []string{"hello", "hello", "hello"}, result)
|
||||
}
|
||||
|
||||
func TestReplicateZero(t *testing.T) {
|
||||
result := u.Replicate(0, 42)
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestReplicateNegative(t *testing.T) {
|
||||
result := u.Replicate(-5, 42)
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestReplicateOne(t *testing.T) {
|
||||
result := u.Replicate(1, 100)
|
||||
assert.Equal(t, []int{100}, result)
|
||||
}
|
||||
18
scan.go
18
scan.go
|
|
@ -1,18 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Scan is like Reduce but returns all intermediate accumulator values.
|
||||
// Also known as prefix scan or cumulative fold.
|
||||
//
|
||||
// Example: Scan([]int{1,2,3,4}, 0, func(acc, n int) int { return acc + n }) → [1, 3, 6, 10]
|
||||
func Scan[T, P any](values []T, acc P, fn func(P, T) P) []P {
|
||||
if len(values) == 0 {
|
||||
return []P{}
|
||||
}
|
||||
|
||||
res := make([]P, 0, len(values))
|
||||
for _, v := range values {
|
||||
acc = fn(acc, v)
|
||||
res = append(res, acc)
|
||||
}
|
||||
return res
|
||||
}
|
||||
68
scan_test.go
68
scan_test.go
|
|
@ -1,68 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestScan(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4}
|
||||
result := u.Scan(nums, 0, func(acc, n int) int { return acc + n })
|
||||
assert.Equal(t, []int{1, 3, 6, 10}, result)
|
||||
}
|
||||
|
||||
func TestScanEmpty(t *testing.T) {
|
||||
result := u.Scan([]int{}, 0, func(acc, n int) int { return acc + n })
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestScanSingleElement(t *testing.T) {
|
||||
result := u.Scan([]int{5}, 0, func(acc, n int) int { return acc + n })
|
||||
assert.Equal(t, []int{5}, result)
|
||||
}
|
||||
|
||||
func TestScanMultiplication(t *testing.T) {
|
||||
nums := []int{2, 3, 4}
|
||||
result := u.Scan(nums, 1, func(acc, n int) int { return acc * n })
|
||||
assert.Equal(t, []int{2, 6, 24}, result)
|
||||
}
|
||||
|
||||
func TestScanStrings(t *testing.T) {
|
||||
words := []string{"hello", "world", "!"}
|
||||
result := u.Scan(words, "", func(acc, s string) string { return acc + s })
|
||||
assert.Equal(t, []string{"hello", "helloworld", "helloworld!"}, result)
|
||||
}
|
||||
|
||||
func TestScanMax(t *testing.T) {
|
||||
nums := []int{3, 1, 4, 1, 5, 9, 2}
|
||||
result := u.Scan(nums, 0, func(acc, n int) int {
|
||||
if n > acc {
|
||||
return n
|
||||
}
|
||||
return acc
|
||||
})
|
||||
assert.Equal(t, []int{3, 3, 4, 4, 5, 9, 9}, result)
|
||||
}
|
||||
|
||||
func TestScanDifferentTypes(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.Scan(nums, 0.0, func(acc float64, n int) float64 {
|
||||
return acc + float64(n)*2.5
|
||||
})
|
||||
assert.Equal(t, []float64{2.5, 7.5, 15.0}, result)
|
||||
}
|
||||
|
||||
func BenchmarkScan(b *testing.B) {
|
||||
nums := make([]int, 1000)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.Scan(nums, 0, func(acc, n int) int { return acc + n })
|
||||
}
|
||||
}
|
||||
23
sliding.go
23
sliding.go
|
|
@ -1,23 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// 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.
|
||||
// Returns an empty slice if size is greater than the slice length.
|
||||
//
|
||||
// Example: Sliding([]int{1,2,3,4,5}, 3) → [[1,2,3], [2,3,4], [3,4,5]]
|
||||
func Sliding[T any](values []T, size int) [][]T {
|
||||
if size <= 0 || size > len(values) {
|
||||
return [][]T{}
|
||||
}
|
||||
|
||||
windowCount := len(values) - size + 1
|
||||
res := make([][]T, 0, windowCount)
|
||||
|
||||
for i := 0; i <= len(values)-size; i++ {
|
||||
window := make([]T, size)
|
||||
copy(window, values[i:i+size])
|
||||
res = append(res, window)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestSliding(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5}
|
||||
result := u.Sliding(nums, 3)
|
||||
expected := [][]int{{1, 2, 3}, {2, 3, 4}, {3, 4, 5}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestSlidingEmpty(t *testing.T) {
|
||||
result := u.Sliding([]int{}, 3)
|
||||
assert.Equal(t, [][]int{}, result)
|
||||
}
|
||||
|
||||
func TestSlidingSizeOne(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.Sliding(nums, 1)
|
||||
expected := [][]int{{1}, {2}, {3}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestSlidingSizeEqualLength(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.Sliding(nums, 3)
|
||||
expected := [][]int{{1, 2, 3}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestSlidingSizeGreaterThanLength(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.Sliding(nums, 5)
|
||||
assert.Equal(t, [][]int{}, result)
|
||||
}
|
||||
|
||||
func TestSlidingSizeZero(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.Sliding(nums, 0)
|
||||
assert.Equal(t, [][]int{}, result)
|
||||
}
|
||||
|
||||
func TestSlidingSizeNegative(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
result := u.Sliding(nums, -1)
|
||||
assert.Equal(t, [][]int{}, result)
|
||||
}
|
||||
|
||||
func TestSlidingTwoElements(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4}
|
||||
result := u.Sliding(nums, 2)
|
||||
expected := [][]int{{1, 2}, {2, 3}, {3, 4}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestSlidingStrings(t *testing.T) {
|
||||
words := []string{"a", "b", "c", "d"}
|
||||
result := u.Sliding(words, 2)
|
||||
expected := [][]string{{"a", "b"}, {"b", "c"}, {"c", "d"}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestSlidingDoesNotMutate(t *testing.T) {
|
||||
original := []int{1, 2, 3, 4}
|
||||
result := u.Sliding(original, 2)
|
||||
|
||||
// Modify a window
|
||||
result[0][0] = 999
|
||||
|
||||
// Original should be unchanged
|
||||
assert.Equal(t, 1, original[0])
|
||||
}
|
||||
|
||||
func BenchmarkSliding(b *testing.B) {
|
||||
nums := make([]int, 100)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.Sliding(nums, 10)
|
||||
}
|
||||
}
|
||||
17
takewhile.go
17
takewhile.go
|
|
@ -1,17 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// 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.
|
||||
func TakeWhile[T any](values []T, predicate func(T) bool) []T {
|
||||
for i, v := range values {
|
||||
if !predicate(v) {
|
||||
res := make([]T, i)
|
||||
copy(res, values[:i])
|
||||
return res
|
||||
}
|
||||
}
|
||||
// All elements satisfy predicate
|
||||
res := make([]T, len(values))
|
||||
copy(res, values)
|
||||
return res
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestTakeWhile(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
result := u.TakeWhile(nums, func(n int) bool { return n < 5 })
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
}
|
||||
|
||||
func TestTakeWhileEmpty(t *testing.T) {
|
||||
result := u.TakeWhile([]int{}, func(n int) bool { return n < 5 })
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestTakeWhileNoneMatch(t *testing.T) {
|
||||
nums := []int{5, 6, 7, 8, 9}
|
||||
result := u.TakeWhile(nums, func(n int) bool { return n < 5 })
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
|
||||
func TestTakeWhileAllMatch(t *testing.T) {
|
||||
nums := []int{1, 2, 3, 4}
|
||||
result := u.TakeWhile(nums, func(n int) bool { return n < 10 })
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
}
|
||||
|
||||
func TestTakeWhileSingleElement(t *testing.T) {
|
||||
result := u.TakeWhile([]int{5}, func(n int) bool { return n < 10 })
|
||||
assert.Equal(t, []int{5}, result)
|
||||
}
|
||||
|
||||
func TestTakeWhileStrings(t *testing.T) {
|
||||
words := []string{"apple", "banana", "cherry", "date"}
|
||||
result := u.TakeWhile(words, func(s string) bool { return len(s) < 6 })
|
||||
assert.Equal(t, []string{"apple"}, result)
|
||||
}
|
||||
|
||||
func BenchmarkTakeWhile(b *testing.B) {
|
||||
nums := make([]int, 1000)
|
||||
for i := range nums {
|
||||
nums[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u.TakeWhile(nums, func(n int) bool { return n < 500 })
|
||||
}
|
||||
}
|
||||
12
tap.go
12
tap.go
|
|
@ -1,12 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Tap applies a function to each element for side effects (like debugging/logging)
|
||||
// and returns the original slice unchanged. Useful for debugging pipelines.
|
||||
//
|
||||
// Example: Tap([]int{1,2,3}, func(n int) { fmt.Println(n) }) → [1,2,3] (and prints each)
|
||||
func Tap[T any](values []T, fn func(T)) []T {
|
||||
for _, v := range values {
|
||||
fn(v)
|
||||
}
|
||||
return values
|
||||
}
|
||||
22
tap_test.go
22
tap_test.go
|
|
@ -1,22 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestTap(t *testing.T) {
|
||||
nums := []int{1, 2, 3}
|
||||
sum := 0
|
||||
result := u.Tap(nums, func(n int) { sum += n })
|
||||
assert.Equal(t, nums, result)
|
||||
assert.Equal(t, 6, sum)
|
||||
}
|
||||
|
||||
func TestTapEmpty(t *testing.T) {
|
||||
result := u.Tap([]int{}, func(n int) {})
|
||||
assert.Equal(t, []int{}, result)
|
||||
}
|
||||
25
transpose.go
25
transpose.go
|
|
@ -1,25 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// 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).
|
||||
//
|
||||
// Example: Transpose([[1,2,3], [4,5,6]]) → [[1,4], [2,5], [3,6]]
|
||||
func Transpose[T any](matrix [][]T) [][]T {
|
||||
if len(matrix) == 0 || len(matrix[0]) == 0 {
|
||||
return [][]T{}
|
||||
}
|
||||
|
||||
rows := len(matrix)
|
||||
cols := len(matrix[0])
|
||||
result := make([][]T, cols)
|
||||
|
||||
for i := range result {
|
||||
result[i] = make([]T, rows)
|
||||
for j := range matrix {
|
||||
result[i][j] = matrix[j][i]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestTranspose(t *testing.T) {
|
||||
matrix := [][]int{{1, 2, 3}, {4, 5, 6}}
|
||||
result := u.Transpose(matrix)
|
||||
expected := [][]int{{1, 4}, {2, 5}, {3, 6}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestTransposeEmpty(t *testing.T) {
|
||||
result := u.Transpose([][]int{})
|
||||
assert.Equal(t, [][]int{}, result)
|
||||
}
|
||||
|
||||
func TestTransposeSquare(t *testing.T) {
|
||||
matrix := [][]int{{1, 2}, {3, 4}}
|
||||
result := u.Transpose(matrix)
|
||||
expected := [][]int{{1, 3}, {2, 4}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
21
unzip.go
21
unzip.go
|
|
@ -1,21 +0,0 @@
|
|||
package underscore
|
||||
|
||||
// Unzip splits a slice of tuples into two separate slices.
|
||||
// The inverse operation of Zip.
|
||||
//
|
||||
// Example: Unzip([Tuple{1,"a"}, Tuple{2,"b"}]) → ([1,2], ["a","b"])
|
||||
func Unzip[L, R any](pairs []Tuple[L, R]) ([]L, []R) {
|
||||
if len(pairs) == 0 {
|
||||
return []L{}, []R{}
|
||||
}
|
||||
|
||||
lefts := make([]L, len(pairs))
|
||||
rights := make([]R, len(pairs))
|
||||
|
||||
for i, pair := range pairs {
|
||||
lefts[i] = pair.Left
|
||||
rights[i] = pair.Right
|
||||
}
|
||||
|
||||
return lefts, rights
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package underscore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
u "github.com/rjNemo/underscore"
|
||||
)
|
||||
|
||||
func TestUnzip(t *testing.T) {
|
||||
pairs := []u.Tuple[int, string]{
|
||||
{Left: 1, Right: "a"},
|
||||
{Left: 2, Right: "b"},
|
||||
{Left: 3, Right: "c"},
|
||||
}
|
||||
lefts, rights := u.Unzip(pairs)
|
||||
assert.Equal(t, []int{1, 2, 3}, lefts)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, rights)
|
||||
}
|
||||
|
||||
func TestUnzipEmpty(t *testing.T) {
|
||||
lefts, rights := u.Unzip([]u.Tuple[int, string]{})
|
||||
assert.Equal(t, []int{}, lefts)
|
||||
assert.Equal(t, []string{}, rights)
|
||||
}
|
||||
Loading…
Reference in a new issue