Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.19

- name: Build
run: go build -v ./...
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

This library implements several high-level functions useful for interacting with slices. It reduces boilerplate required by for-loops and variable initializations.

Library takes advantage of Go generics increasing usability by compile-time type-safety. Go version of at least **1.18** is therefore required.
Go version of at least **1.19** is required.

## Examples

Expand Down Expand Up @@ -83,11 +83,11 @@ dedup := Deduplicate(slice)

### >> _All_

Returns `true` if all slice elements are evaluated `true` with given argument function.
Returns `true` if all slice elements are evaluated to `true` with given argument function.

### >> _Any_

Returns `true` if any slice element is evaluated `true` with given argument function.
Returns `true` if any slice element evaluates to `true` with given argument function.

### >> _AreDisjoint_

Expand Down Expand Up @@ -213,6 +213,14 @@ Calculates a union set from two slice sets.

## List of parallel functions

### >> _ParAll_

Returns `true` if all slice elements are evaluated to `true` with given argument function. Evaluation may be performed in parallel if system has multiple logical CPUs.

### >> _ParAny_

Returns `true` if any slice element evaluates to `true` with given argument function. Evaluation may be performed in parallel if system has multiple logical CPUs.

### >> _ParMap_

Maps each element through argument function which can modify their type and/or value. Evenly distributes the mapping operation to multiple goroutines. The number of used goroutines is equal to the available number of logical processors.
Expand Down
10 changes: 9 additions & 1 deletion common.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func newSliceDivGen(length, divs int) sliceDivGen {
// sub-slice index.
//
// `divIdx` is expected to be less than the number of divisions.
func (sdg sliceDivGen) get(divIdx int) (int, int) {
func (sdg sliceDivGen) offsetAndLength(divIdx int) (int, int) {
if divIdx < sdg.firstPartDivs {
offset := (sdg.minDivLen + 1) * divIdx
return offset, sdg.minDivLen + 1
Expand All @@ -48,3 +48,11 @@ func (sdg sliceDivGen) get(divIdx int) (int, int) {
return offset, sdg.minDivLen
}
}

// Gets start and end indexes in the original slice for given sub-slice index.
//
// `divIdx` is expected to be less than the number of divisions.
func (sdg sliceDivGen) startAndEnd(divIdx int) (int, int) {
offset, length := sdg.offsetAndLength(divIdx)
return offset, offset + length
}
2 changes: 1 addition & 1 deletion common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func TestSliceDivGen(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
for i, expected := range testCase.expects {
offset, length := testCase.gen.get(i)
offset, length := testCase.gen.offsetAndLength(i)
assert.Equal(t, expected.offset, offset)
assert.Equal(t, expected.length, length)
}
Expand Down
72 changes: 68 additions & 4 deletions sliceutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sliceutils
import (
"runtime"
"sync"
"sync/atomic"
)

// Returns true if all slice elements are evaluated true with given evaluator
Expand Down Expand Up @@ -488,6 +489,70 @@ func Union[T comparable](lhs, rhs []T) []T {
// PARALLEL FUNCTIONS //
////////////////////////

// Returns true if all slice elements are evaluated to true with given
// evaluator function. Slice processing is divided evenly to number of
// goroutines.
//
// Returns as soon as element evaluating to `false` is found e.g. function is
// short-circuiting.
//
// Order of evaluations is undefined and shall not be depended on. Returns true
// on nil slice. Panics on nil evaluator function.
func ParAll[T any](slice []T, allFn func(T) bool) bool {
// Divide slice by the number of logical CPUs.
divs := runtime.NumCPU()
sliceDivGen := newSliceDivGen(len(slice), divs)

// Create a waitgroup for waiting goroutines to finish.
var wg sync.WaitGroup
wg.Add(divs)

// Initialize result flag.
var allFlag atomic.Bool
allFlag.Store(true)

// Loop over all divisions.
for divIdx := 0; divIdx < divs; divIdx++ {
// Start goroutine for processing a sub-slice.
go func(divIdx int) {
// Notify goroutine has finished in the end.
defer wg.Done()

// Loop over sub-slice and check if `false` element is found.
// Checking flag after every iteration allows early termination with O(n)
// execution time instead of Θ(n).
// Question still exists whether the flag is worth checking after every
// iteration or some variable N amount of iterations.
start, end := sliceDivGen.startAndEnd(divIdx)
for i := start; i < end && allFlag.Load(); i++ {
if !allFn(slice[i]) {
// `false` element was found. Stop processing the slice.
allFlag.Store(false)
return
}
}
}(divIdx)
}
// Wait until all goroutines have finished.
wg.Wait()

// Return result.
return allFlag.Load()
}

// Returns true if any slice element is evaluated to true with given
// evaluator function. Slice processing may be divided to number of goroutines.
//
// Returns as soon as element evaluating to `true` is found e.g. function is
// short-circuiting.
//
// Order of evaluations is undefined and shall not be depended on. Returns false
// on nil slice. Panics on nil evaluator function.
func ParAny[T any](slice []T, anyFn func(T) bool) bool {
// Call ParAll just inverting all results.
return !ParAll(slice, func(val T) bool { return !anyFn(val) })
}

// Maps each slice value with a mapping function and divides the slice by the
// number of logical processors to evenly distribute work.
//
Expand All @@ -510,16 +575,15 @@ func ParMap[T, U any](slice []T, mapFn func(T) U) []U {
var wg sync.WaitGroup
wg.Add(divs)

// Loop all divisions
// Loop over all divisions.
for divIdx := 0; divIdx < divs; divIdx++ {
// Start goroutine for mapping a sub-slice.
go func(divIdx int) {
// Notify goroutine has finished mapping in the end.
defer wg.Done()

// Get division specific offset and length for the sub-slice.
offset, length := sliceDivGen.get(divIdx)
start, end := offset, offset+length
// Get division specific start and end indexes for the sub-slice.
start, end := sliceDivGen.startAndEnd(divIdx)

// Map.
mappedSubSlice := Map(slice[start:end], mapFn)
Expand Down
56 changes: 56 additions & 0 deletions sliceutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,62 @@ func TestUnion(t *testing.T) {
// PARALLEL FUNCTIONS //
////////////////////////

func TestParAll(t *testing.T) {
t.Run("All elements evaluate to true", func(t *testing.T) {
slice := []int{1, 4, 6, 2, 3, 7}
allPositive := ParAll(slice, func(i int) bool { return i > 0 })
assert.True(t, allPositive)
})

t.Run("Some elements don't evaluate to true", func(t *testing.T) {
slice := []int{1, 4, 6, -2, 3, 7}
allPositive := ParAll(slice, func(i int) bool { return i > 0 })
assert.False(t, allPositive)
})

t.Run("An element evaluates to false on large array", func(t *testing.T) {
slice := Generate(1000, func(idx int) int { return idx + 1 })
slice[500] = 0

allPositive := ParAll(slice, func(i int) bool { return i > 0 })
assert.False(t, allPositive)
})

t.Run("Return true on nil slice", func(t *testing.T) {
var slice []int = nil
allPositive := ParAll(slice, func(i int) bool { return i > 0 })
assert.True(t, allPositive)
})
}

func TestParAny(t *testing.T) {
t.Run("Some elements evaluate to true", func(t *testing.T) {
slice := []int{-1, -4, 6, -2, 3, 7}
anyPositive := ParAny(slice, func(i int) bool { return i > 0 })
assert.True(t, anyPositive)
})

t.Run("All elements evaluate to false", func(t *testing.T) {
slice := []int{-1, -4, -6, -2, -3, -7}
anyPositive := ParAny(slice, func(i int) bool { return i > 0 })
assert.False(t, anyPositive)
})

t.Run("An element evaluates to true on large array", func(t *testing.T) {
slice := Generate(1000, func(idx int) int { return idx + 1 })
slice[500] = 0

anyPositive := ParAny(slice, func(i int) bool { return i == 0 })
assert.True(t, anyPositive)
})

t.Run("Return false on nil slice", func(t *testing.T) {
var slice []int = nil
anyPositive := ParAny(slice, func(i int) bool { return i > 0 })
assert.False(t, anyPositive)
})
}

func TestParMap(t *testing.T) {
t.Run("Increment int values by one in large array", func(t *testing.T) {
slice := Generate(1000, func(idx int) int { return idx })
Expand Down