├── perf ├── Makefile ├── README.md └── perf_test.go ├── FuncFrogIco.jpg ├── Makefile ├── internal ├── primitive │ └── pointer │ │ ├── pointer.go │ │ └── pointer_test.go ├── internalpipe │ ├── erase.go │ ├── promices.go │ ├── reduce.go │ ├── filter.go │ ├── erase_test.go │ ├── map.go │ ├── promices_test.go │ ├── snag.go │ ├── mapfilter.go │ ├── sort.go │ ├── sum.go │ ├── yeet.go │ ├── snag_test.go │ ├── sort_test.go │ ├── yeet_test.go │ ├── do.go │ ├── map_test.go │ ├── do_test.go │ ├── pipe.go │ ├── filter_test.go │ ├── first_test.go │ ├── constructor.go │ ├── sum_test.go │ ├── any.go │ ├── reduce_test.go │ ├── first.go │ ├── mapfilterer_test.go │ ├── constructor_test.go │ ├── any_test.go │ └── pipe_test.go └── algo │ ├── batch │ ├── batch.go │ └── batch_test.go │ └── parallel │ ├── mergesort │ ├── mergesort_test.go │ └── mergesort.go │ └── qsort │ ├── qsort.go │ └── qsort_test.go ├── pkg ├── ff │ ├── compose.go │ ├── filter.go │ ├── map.go │ ├── reduce.go │ ├── map_filter.go │ └── ff_test.go ├── pipies │ ├── comparators.go │ ├── reducers.go │ ├── not.go │ ├── filters.go │ └── pipies_test.go └── pipe │ ├── yeet.go │ ├── collectors.go │ ├── functype.go │ ├── interface.go │ ├── pipenl.go │ ├── constructors.go │ ├── prefixpipe.go │ ├── pipe.go │ └── pipe_test.go ├── .gitignore ├── go.mod ├── .github └── workflows │ ├── go.yml │ └── coverage-badge.yml ├── coverage.svg ├── LICENSE ├── go.sum └── README.md /perf/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go test -bench=. 3 | -------------------------------------------------------------------------------- /FuncFrogIco.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koss-null/FuncFrog/HEAD/FuncFrogIco.jpg -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | example: 2 | go run ./example/main.go 3 | 4 | test: 5 | go test -race -count=1 --parallel 8 ./... 6 | 7 | get_coverage_pic: 8 | gopherbadger -md="README.md,coverage.out" 9 | -------------------------------------------------------------------------------- /internal/primitive/pointer/pointer.go: -------------------------------------------------------------------------------- 1 | package pointer 2 | 3 | func Ref[T any](x T) *T { 4 | return &x 5 | } 6 | 7 | func Deref[T any](x *T) (res T) { 8 | if x == nil { 9 | return 10 | } 11 | return *x 12 | } 13 | -------------------------------------------------------------------------------- /pkg/ff/compose.go: -------------------------------------------------------------------------------- 1 | package ff 2 | 3 | // Compose creates a function wich is a composition of two functions. 4 | func Compose[T1, T2, T3 any](fn1 func(T1) T2, fn2 func(T2) T3) func(T1) T3 { 5 | return func(x T1) T3 { 6 | return fn2(fn1(x)) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /pkg/ff/filter.go: -------------------------------------------------------------------------------- 1 | package ff 2 | 3 | import "github.com/koss-null/funcfrog/pkg/pipe" 4 | 5 | // Filter is a short way to create a Pipe from a slice of SrcT applying Filter function fn. 6 | func Filter[SrcT any](a []SrcT, fn func(*SrcT) bool) pipe.Piper[SrcT] { 7 | return pipe.Slice(a).Filter(fn) 8 | } 9 | -------------------------------------------------------------------------------- /pkg/ff/map.go: -------------------------------------------------------------------------------- 1 | package ff 2 | 3 | import "github.com/koss-null/funcfrog/pkg/pipe" 4 | 5 | // Map is a short way to create a Pipe of DstT from a slice of SrcT applying Map function fn. 6 | func Map[SrcT, DstT any](a []SrcT, fn func(SrcT) DstT) pipe.Piper[DstT] { 7 | return pipe.Map(pipe.Slice(a), fn) 8 | } 9 | -------------------------------------------------------------------------------- /pkg/ff/reduce.go: -------------------------------------------------------------------------------- 1 | package ff 2 | 3 | import "github.com/koss-null/funcfrog/pkg/pipe" 4 | 5 | // Reduce is a short way to create DstT value from a slice of SrcT applying Reduce function fn. 6 | func Reduce[SrcT any, DstT any](a []SrcT, fn func(*DstT, *SrcT) DstT, initVal ...DstT) DstT { 7 | return pipe.Reduce(pipe.Slice(a), fn) 8 | } 9 | -------------------------------------------------------------------------------- /pkg/pipies/comparators.go: -------------------------------------------------------------------------------- 1 | // Function set to use with Sort 2 | package pipies 3 | 4 | import "golang.org/x/exp/constraints" 5 | 6 | // The list of comparators to be used for Sort() method. 7 | 8 | // Less returns true if x < y, false otherwise. 9 | func Less[T constraints.Ordered](x, y *T) bool { 10 | return *x < *y 11 | } 12 | -------------------------------------------------------------------------------- /pkg/ff/map_filter.go: -------------------------------------------------------------------------------- 1 | package ff 2 | 3 | import "github.com/koss-null/funcfrog/pkg/pipe" 4 | 5 | // MapFilter is a short way to create a Pipe of DstT from a slice of SrcT applying MapFilter function fn. 6 | func MapFilter[SrcT, DstT any](a []SrcT, fn func(SrcT) (DstT, bool)) pipe.Piper[DstT] { 7 | return pipe.MapFilter(pipe.Slice(a), fn) 8 | } 9 | -------------------------------------------------------------------------------- /pkg/pipies/reducers.go: -------------------------------------------------------------------------------- 1 | // Function set to use with Reduce 2 | package pipies 3 | 4 | import ( 5 | cns "golang.org/x/exp/constraints" 6 | ) 7 | 8 | // Sum is a reduce function for summing up types that support "+". 9 | func Sum[T cns.Float | cns.Integer | cns.Complex | ~string](a, b *T) T { 10 | res := *a + *b 11 | return res 12 | } 13 | -------------------------------------------------------------------------------- /pkg/pipe/yeet.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "github.com/koss-null/funcfrog/internal/internalpipe" 5 | ) 6 | 7 | const ( 8 | initErrsAmount = 10 9 | initHandlersAmount = 5 10 | ) 11 | 12 | // NewYeti creates a brand new Yeti - an object for error handling. 13 | func NewYeti() internalpipe.YeetSnag { 14 | return internalpipe.NewYeti() 15 | } 16 | -------------------------------------------------------------------------------- /internal/internalpipe/erase.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | func (p Pipe[T]) Erase() Pipe[any] { 4 | return Pipe[any]{ 5 | Fn: func(i int) (*any, bool) { 6 | if obj, skipped := p.Fn(i); !skipped { 7 | anyObj := any(obj) 8 | return &anyObj, false 9 | } 10 | return nil, true 11 | }, 12 | Len: p.Len, 13 | ValLim: p.ValLim, 14 | GoroutinesCnt: p.GoroutinesCnt, 15 | 16 | y: p.y, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/internalpipe/promices.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | func (p Pipe[T]) Promices() []func() (T, bool) { 4 | limit := p.limit() 5 | proms := make([]func() (T, bool), limit) 6 | var empty T 7 | for i := 0; i < limit; i++ { 8 | cpi := i 9 | proms[i] = func() (T, bool) { 10 | obj, skipped := p.Fn(cpi) 11 | if skipped { 12 | return empty, false 13 | } 14 | return *obj, true 15 | } 16 | } 17 | return proms 18 | } 19 | -------------------------------------------------------------------------------- /internal/algo/batch/batch.go: -------------------------------------------------------------------------------- 1 | package batch 2 | 3 | import "math" 4 | 5 | func min(a, b int) int { 6 | if a > b { 7 | return b 8 | } 9 | return a 10 | } 11 | 12 | func Do[T any](a []T, batchSize int) [][]T { 13 | batchCnt := int(math.Ceil(float64(len(a)) / float64(batchSize))) 14 | res := make([][]T, batchCnt) 15 | j := 0 16 | for i := 0; i < len(a); i += batchSize { 17 | res[j] = a[i:min(i+batchSize, len(a))] 18 | j++ 19 | } 20 | return res 21 | } 22 | -------------------------------------------------------------------------------- /internal/algo/batch/batch_test.go: -------------------------------------------------------------------------------- 1 | package batch_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/koss-null/funcfrog/internal/algo/batch" 9 | ) 10 | 11 | func Test_BatchDo(t *testing.T) { 12 | a := []int{1, 2, 3, 4, 5, 6, 7} 13 | b := batch.Do(a, 3) 14 | require.Equal(t, len(b), 3) 15 | require.Equal(t, b[0], []int{1, 2, 3}) 16 | require.Equal(t, b[1], []int{4, 5, 6}) 17 | require.Equal(t, b[2], []int{7}) 18 | } 19 | -------------------------------------------------------------------------------- /internal/internalpipe/reduce.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | type AccumFn[T any] func(*T, *T) T 4 | 5 | // Reduce applies the result of a function to each element one-by-one: f(p[n], f(p[n-1], f(p[n-2, ...]))). 6 | func (p Pipe[T]) Reduce(fn AccumFn[T]) *T { 7 | data := p.Do() 8 | switch len(data) { 9 | case 0: 10 | return nil 11 | case 1: 12 | return &data[0] 13 | default: 14 | res := data[0] 15 | for _, val := range data[1:] { 16 | res = fn(&res, &val) 17 | } 18 | return &res 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X files 2 | .DS_Store 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | *.pprof 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 19 | .glide/ 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | cover.cov 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/koss-null/funcfrog 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/pkg/profile v1.7.0 7 | github.com/stretchr/testify v1.8.0 8 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/felixge/fgprof v0.9.3 // indirect 14 | github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /internal/internalpipe/filter.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | // Filter leaves only items with true predicate fn. 4 | func (p Pipe[T]) Filter(fn func(*T) bool) Pipe[T] { 5 | return Pipe[T]{ 6 | Fn: func(i int) (*T, bool) { 7 | if obj, skipped := p.Fn(i); !skipped { 8 | if !fn(obj) { 9 | return nil, true 10 | } 11 | return obj, false 12 | } 13 | return nil, true 14 | }, 15 | Len: p.Len, 16 | ValLim: p.ValLim, 17 | GoroutinesCnt: p.GoroutinesCnt, 18 | 19 | y: p.y, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/internalpipe/erase_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestErase(t *testing.T) { 10 | t.Parallel() 11 | 12 | a := Func(func(i int) (int, bool) { 13 | if i%5 == 0 { 14 | return 10, false 15 | } 16 | return i, true 17 | }).Gen(100) 18 | 19 | er := a.Erase() 20 | res := er.Do() 21 | idx := 0 22 | for i := 0; i < 100; i++ { 23 | if i%5 != 0 { 24 | require.Equal(t, i, *(res[idx].(*int))) 25 | idx++ 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/internalpipe/map.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | // Map applies given function to each element of the underlying slice 4 | // returns the slice where each element is n[i] = f(p[i]). 5 | func (p Pipe[T]) Map(fn func(T) T) Pipe[T] { 6 | return Pipe[T]{ 7 | Fn: func(i int) (*T, bool) { 8 | if obj, skipped := p.Fn(i); !skipped { 9 | res := fn(*obj) 10 | return &res, false 11 | } 12 | return nil, true 13 | }, 14 | Len: p.Len, 15 | ValLim: p.ValLim, 16 | GoroutinesCnt: p.GoroutinesCnt, 17 | 18 | y: p.y, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/primitive/pointer/pointer_test.go: -------------------------------------------------------------------------------- 1 | package pointer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Pointer(t *testing.T) { 10 | t.Parallel() 11 | 12 | t.Run("To", func(t *testing.T) { 13 | require.Equal(t, 10, *Ref(10)) 14 | require.Equal(t, 10., *Ref(10.)) 15 | s := struct{ a, b int }{1, 2} 16 | require.Equal(t, s, *Ref(s)) 17 | }) 18 | 19 | t.Run("From", func(t *testing.T) { 20 | require.Equal(t, 10, Deref(Ref(10))) 21 | require.Equal(t, 10., Deref(Ref(10.))) 22 | require.Equal(t, 0, Deref[int](nil)) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /internal/internalpipe/promices_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestPromices(t *testing.T) { 10 | t.Parallel() 11 | 12 | a := Func(func(i int) (int, bool) { 13 | return i, true 14 | }).Filter(func(x *int) bool { return *x != 100 }).Take(101) 15 | 16 | proms := a.Promices() 17 | for i := 0; i < 100; i++ { 18 | res, ok := proms[i]() 19 | require.True(t, ok) 20 | if i == 100 { 21 | require.Equal(t, i+1, res) 22 | continue 23 | } 24 | require.Equal(t, i, res) 25 | } 26 | _, ok := proms[100]() 27 | require.False(t, ok) 28 | } 29 | -------------------------------------------------------------------------------- /internal/internalpipe/snag.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | // Sang ads error handler to a current Pipe step. 4 | func (p Pipe[T]) Snag(h ErrHandler) Pipe[T] { 5 | if p.y == nil { 6 | return p 7 | } 8 | 9 | p.y.Snag(h) 10 | return p 11 | } 12 | 13 | type YeetSnag interface { 14 | // yeet an error 15 | Yeet(err error) 16 | // snag and handle the error 17 | Snag(ErrHandler) 18 | } 19 | 20 | // Yeti adds Yeti error handler to the pipe. 21 | // If some other handlers were set before, they are handled by the Snag 22 | func (p Pipe[T]) Yeti(y YeetSnag) Pipe[T] { 23 | yet := y.(yeti) 24 | if p.y != nil { 25 | yet.AddYeti(p.y) 26 | } 27 | p.y = yet 28 | return p 29 | } 30 | -------------------------------------------------------------------------------- /internal/internalpipe/mapfilter.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | // MapFilter applies given function to each element of the underlying slice, 4 | // if the second returning value of fn is false, the element is skipped (may be useful for error handling). 5 | // returns the slice where each element is n[i] = f(p[i]) if it is not skipped. 6 | func (p Pipe[T]) MapFilter(fn func(T) (T, bool)) Pipe[T] { 7 | return Pipe[T]{ 8 | Fn: func(i int) (*T, bool) { 9 | if obj, skipped := p.Fn(i); !skipped { 10 | res, take := fn(*obj) 11 | return &res, !take 12 | } 13 | return nil, true 14 | }, 15 | Len: p.Len, 16 | ValLim: p.ValLim, 17 | GoroutinesCnt: p.GoroutinesCnt, 18 | 19 | y: p.y, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/internalpipe/sort.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/koss-null/funcfrog/internal/algo/parallel/qsort" 7 | ) 8 | 9 | // Sort sorts the underlying slice on a current step of a pipeline. 10 | func (p Pipe[T]) Sort(less func(*T, *T) bool) Pipe[T] { 11 | var once sync.Once 12 | var sorted []T 13 | 14 | return Pipe[T]{ 15 | Fn: func(i int) (*T, bool) { 16 | once.Do(func() { 17 | data := p.Do() 18 | if len(data) == 0 { 19 | return 20 | } 21 | sorted = qsort.Sort(data, less, p.GoroutinesCnt) 22 | }) 23 | if i >= len(sorted) { 24 | return nil, true 25 | } 26 | return &sorted[i], false 27 | }, 28 | Len: p.Len, 29 | ValLim: p.ValLim, 30 | GoroutinesCnt: p.GoroutinesCnt, 31 | 32 | y: p.y, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Set up Go 11 | uses: actions/setup-go@v3 12 | with: 13 | go-version: 1.21 14 | 15 | - name: Test 16 | run: make test 17 | 18 | coverage: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version: 1.21 26 | - uses: gwatts/go-coverage-action@v1 27 | id: coverage 28 | with: 29 | coverage-threshold: 90 30 | # collect coverage for all packages beyond the one under test 31 | cover-pkg: ./... 32 | # Ignore code-generated files when calculating coverage totals 33 | ignore-pattern: | 34 | \.pb\.go$ 35 | -------------------------------------------------------------------------------- /pkg/pipies/not.go: -------------------------------------------------------------------------------- 1 | package pipies 2 | 3 | // This madness is the only way to implement Not function for a function with arbitary amount of arguments. 4 | // The amount of t at the end of Not(t) function equals to the amount of arguments the initial function takes 5 | 6 | // Not returns a new function that negates the result of the input function. 7 | func Not[T any](fn func(x T) bool) func(T) bool { 8 | return func(x T) bool { 9 | return !fn(x) 10 | } 11 | } 12 | 13 | // Nott returns a new function that negates the result of the input function. 14 | func Nott[T1, T2 any](fn func(x T1, y T2) bool) func(T1, T2) bool { 15 | return func(x T1, y T2) bool { 16 | return !fn(x, y) 17 | } 18 | } 19 | 20 | // Nottt returns a new function that negates the result of the input function. 21 | func Nottt[T1, T2, T3 any](fn func(x T1, y T2, z T3) bool) func(T1, T2, T3) bool { 22 | return func(x T1, y T2, z T3) bool { 23 | return !fn(x, y, z) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /coverage.svg: -------------------------------------------------------------------------------- 1 | coverage: 99.4%coverage99.4% -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dima Koss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/pipe/collectors.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import "github.com/koss-null/funcfrog/internal/internalpipe" 4 | 5 | // Collect translates Piper with erased type (achieved by calling an Erase method of any type). 6 | func Collect[DstT any](p Piper[any]) Piper[DstT] { 7 | pp := any(p).(entrails[any]).Entrails() 8 | return &Pipe[DstT]{internalpipe.Pipe[DstT]{ 9 | Fn: func(i int) (*DstT, bool) { 10 | if obj, skipped := pp.Fn(i); !skipped { 11 | dst, ok := (*obj).(*DstT) 12 | return dst, !ok 13 | } 14 | return nil, true 15 | }, 16 | Len: pp.Len, 17 | ValLim: pp.ValLim, 18 | GoroutinesCnt: pp.GoroutinesCnt, 19 | }} 20 | } 21 | 22 | // CollectNL translates PiperNL with erased type (achieved by calling an Erase method of any type). 23 | func CollectNL[DstT any](p PiperNoLen[any]) PiperNoLen[DstT] { 24 | pp := any(p).(entrails[any]).Entrails() 25 | return &PipeNL[DstT]{internalpipe.Pipe[DstT]{ 26 | Fn: func(i int) (*DstT, bool) { 27 | if obj, skipped := pp.Fn(i); !skipped { 28 | dst, ok := (*obj).(*DstT) 29 | return dst, !ok 30 | } 31 | return nil, true 32 | }, 33 | Len: pp.Len, 34 | ValLim: pp.ValLim, 35 | GoroutinesCnt: pp.GoroutinesCnt, 36 | }} 37 | } 38 | -------------------------------------------------------------------------------- /internal/internalpipe/sum.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import "sync" 4 | 5 | func sumSingleThread[T any](length int, plus AccumFn[T], fn GeneratorFn[T]) T { 6 | var res T 7 | var obj *T 8 | var skipped bool 9 | i := 0 10 | for ; i < length; i++ { 11 | obj, skipped = fn(i) 12 | if !skipped { 13 | res = *obj 14 | i++ 15 | break 16 | } 17 | } 18 | 19 | for ; i < length; i++ { 20 | obj, skipped = fn(i) 21 | if !skipped { 22 | res = plus(&res, obj) 23 | } 24 | } 25 | return res 26 | } 27 | 28 | // Sum returns the sum of all elements. It is similar to Reduce but is able to work in parallel. 29 | func (p Pipe[T]) Sum(plus AccumFn[T]) T { 30 | length := p.limit() 31 | if p.GoroutinesCnt == 1 { 32 | return sumSingleThread(length, plus, p.Fn) 33 | } 34 | 35 | var ( 36 | step = divUp(length, p.GoroutinesCnt) 37 | 38 | res T 39 | resMx sync.Mutex 40 | wg sync.WaitGroup 41 | ) 42 | 43 | sum := func(x *T) { 44 | resMx.Lock() 45 | res = plus(&res, x) 46 | resMx.Unlock() 47 | } 48 | 49 | tickets := genTickets(p.GoroutinesCnt) 50 | for lf := 0; lf < length; lf += step { 51 | wg.Add(1) 52 | <-tickets 53 | go func(lf, rg int) { 54 | var inRes T 55 | var obj *T 56 | var skipped bool 57 | for i := lf; i < rg; i++ { 58 | if obj, skipped = p.Fn(i); !skipped { 59 | inRes = plus(&inRes, obj) 60 | } 61 | } 62 | sum(&inRes) 63 | wg.Done() 64 | tickets <- struct{}{} 65 | }(lf, min(lf+step, length)) 66 | } 67 | wg.Wait() 68 | 69 | return res 70 | } 71 | -------------------------------------------------------------------------------- /internal/internalpipe/yeet.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type ErrHandler func(error) 8 | 9 | type Yeti struct { 10 | eMx *sync.Mutex 11 | errs []error 12 | hMx *sync.Mutex 13 | handlers []ErrHandler 14 | yMx *sync.Mutex 15 | yetis []yeti 16 | } 17 | 18 | func NewYeti() *Yeti { 19 | const yetiExpectedErrors = 6 20 | return &Yeti{ 21 | errs: make([]error, 0, yetiExpectedErrors), 22 | handlers: make([]ErrHandler, 0, yetiExpectedErrors), 23 | yetis: make([]yeti, 0), 24 | eMx: &sync.Mutex{}, 25 | hMx: &sync.Mutex{}, 26 | yMx: &sync.Mutex{}, 27 | } 28 | } 29 | 30 | func (y *Yeti) Yeet(err error) { 31 | y.eMx.Lock() 32 | y.errs = append(y.errs, err) 33 | y.eMx.Unlock() 34 | } 35 | 36 | func (y *Yeti) Snag(handler ErrHandler) { 37 | y.hMx.Lock() 38 | y.handlers = append(y.handlers, handler) 39 | y.hMx.Unlock() 40 | } 41 | 42 | func (y *Yeti) Handle() { 43 | y.yMx.Lock() 44 | prevYs := y.yetis 45 | y.yMx.Unlock() 46 | for _, prevYetti := range prevYs { 47 | prevYetti.Handle() 48 | } 49 | 50 | y.hMx.Lock() 51 | y.eMx.Lock() 52 | defer y.hMx.Unlock() 53 | defer y.eMx.Unlock() 54 | 55 | for _, err := range y.errs { 56 | for _, handle := range y.handlers { 57 | handle(err) 58 | } 59 | } 60 | } 61 | 62 | func (y *Yeti) AddYeti(yt yeti) { 63 | y.yMx.Lock() 64 | y.yetis = append(y.yetis, yt) 65 | y.yMx.Unlock() 66 | } 67 | 68 | type yeti interface { 69 | Yeet(err error) 70 | Snag(h ErrHandler) 71 | // TODO: Handle should be called after each Pipe function eval 72 | Handle() 73 | AddYeti(y yeti) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/pipe/functype.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | // Accum is a standard type for reduce function. 4 | // It is guaranteed that arguments are not nil, so you can dereference them with no check. 5 | type Accum[T any] func(*T, *T) T 6 | 7 | // Acc creates an Accum from a function of a different signature. 8 | // Using it allows inner function `fn` not to use dereference, 9 | // but also it leads to a larger memory allocation. 10 | func Acc[T any](fn func(T, T) T) Accum[T] { 11 | return func(x, y *T) T { 12 | res := fn(*x, *y) 13 | return res 14 | } 15 | } 16 | 17 | // Predicate is a standard type for filtering. 18 | // It is guaranteed that argument is not nil, so you can dereference it with no check. 19 | type Predicate[T any] func(*T) bool 20 | 21 | // Pred creates a Predicate from a function of a different signature. 22 | // Using it allows inner function `fn` not to use dereference, 23 | // but also it leads to a larger memory allocation. 24 | func Pred[T any](fn func(T) bool) Predicate[T] { 25 | return func(x *T) bool { 26 | return fn(*x) 27 | } 28 | } 29 | 30 | // Comparator is a standard type for sorting comparisons. 31 | // It is guaranteed that argument is not nil, so you can dereference it with no check. 32 | type Comparator[T any] func(*T, *T) bool 33 | 34 | // Comp creates a Comparator from a function of a different signature. 35 | // Using it allows inner function `cmp` not to use dereference, 36 | // but also it leads to a larger memory allocation. 37 | func Comp[T any](cmp func(T, T) bool) Comparator[T] { 38 | return func(x, y *T) bool { 39 | return cmp(*x, *y) 40 | } 41 | } 42 | 43 | // Promice returns two values: the evaluated value and if it is not skipped. 44 | // It should be checked as: if p, notSkipped := promice(); notSkipped { appendToAns(p) } 45 | type Promice[T any] func() (T, bool) 46 | -------------------------------------------------------------------------------- /internal/internalpipe/snag_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestPipe_Snag(t *testing.T) { 11 | t.Parallel() 12 | 13 | t.Run("empty yeti", func(t *testing.T) { 14 | t.Parallel() 15 | 16 | handlerCalled := false 17 | _ = Func(func(i int) (int, bool) { 18 | return i, true 19 | }).Take(1000).Snag(func(_ error) { handlerCalled = true }).Do() 20 | require.False(t, handlerCalled) 21 | }) 22 | 23 | t.Run("happy yeti snag", func(t *testing.T) { 24 | t.Parallel() 25 | 26 | yeti := NewYeti() 27 | handlerCalled := false 28 | _ = Func(func(i int) (int, bool) { 29 | if i == 10 { 30 | yeti.Yeet(fmt.Errorf("failed on %d", i)) 31 | return i, false 32 | } 33 | return i, true 34 | }).Yeti(yeti).Snag(func(_ error) { handlerCalled = true }).Take(1000).Do() 35 | require.True(t, handlerCalled) 36 | }) 37 | 38 | t.Run("happy double yeti snag", func(t *testing.T) { 39 | t.Parallel() 40 | 41 | yeti, yeti2 := NewYeti(), NewYeti() 42 | handlerCalled := false 43 | _ = Func(func(i int) (int, bool) { 44 | if i == 10 { 45 | yeti.Yeet(fmt.Errorf("failed on %d", i)) 46 | return i, false 47 | } 48 | return i, true 49 | }).Yeti(yeti2).Yeti(yeti).Snag(func(_ error) { handlerCalled = true }).Take(1000).Do() 50 | require.True(t, handlerCalled) 51 | }) 52 | 53 | t.Run("yeti not set no snag handled", func(t *testing.T) { 54 | t.Parallel() 55 | 56 | yeti := NewYeti() 57 | handlerCalled := false 58 | _ = Func(func(i int) (int, bool) { 59 | if i == 10 { 60 | yeti.Yeet(fmt.Errorf("failed on %d", i)) 61 | return i, false 62 | } 63 | return i, true 64 | }).Snag(func(_ error) { handlerCalled = true }).Take(1000).Do() 65 | require.False(t, handlerCalled) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /internal/internalpipe/sort_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Sort(t *testing.T) { 11 | exp := make([]int, 100_000) 12 | for i := 0; i < 100_000; i++ { 13 | exp[i] = i 14 | } 15 | a := make([]int, 100_000) 16 | copy(a, exp) 17 | rand.Shuffle(len(a), func(i, j int) { 18 | a[i], a[j] = a[j], a[i] 19 | }) 20 | 21 | neq := func(exp, a []int) bool { 22 | for i := range exp { 23 | if exp[i] != a[i] { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | t.Run("single thread", func(t *testing.T) { 31 | require.True(t, neq(exp, a)) 32 | p := Func(func(i int) (int, bool) { 33 | return a[i], true 34 | }). 35 | Take(100_000). 36 | Sort(func(x, y *int) bool { return *x < *y }). 37 | Do() 38 | require.Equal(t, len(exp), len(p)) 39 | for i := range p { 40 | require.Equal(t, exp[i], p[i]) 41 | } 42 | }) 43 | 44 | t.Run("seven thread", func(t *testing.T) { 45 | require.True(t, neq(exp, a)) 46 | p := Func(func(i int) (int, bool) { 47 | return a[i], true 48 | }). 49 | Take(100_000). 50 | Sort(func(x, y *int) bool { return *x < *y }). 51 | Parallel(7). 52 | Do() 53 | require.Equal(t, len(exp), len(p)) 54 | for i := range p { 55 | require.Equal(t, exp[i], p[i]) 56 | } 57 | }) 58 | 59 | t.Run("single thread empty", func(t *testing.T) { 60 | require.True(t, neq(exp, a)) 61 | p := Func(func(i int) (int, bool) { 62 | return a[i], false 63 | }). 64 | Gen(10). 65 | Sort(func(x, y *int) bool { return *x < *y }). 66 | Do() 67 | require.Equal(t, []int{}, p) 68 | }) 69 | 70 | t.Run("seven thread empty", func(t *testing.T) { 71 | require.True(t, neq(exp, a)) 72 | p := Func(func(i int) (int, bool) { 73 | return a[i], false 74 | }). 75 | Gen(10). 76 | Sort(func(x, y *int) bool { return *x < *y }). 77 | Parallel(7). 78 | Do() 79 | require.Equal(t, []int{}, p) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/ff/ff_test.go: -------------------------------------------------------------------------------- 1 | package ff 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCompose(t *testing.T) { 11 | fn1 := func(x int) int { 12 | return x + 1 13 | } 14 | 15 | fn2 := func(x int) string { 16 | return strconv.Itoa(x) 17 | } 18 | 19 | composedFn := Compose(fn1, fn2) 20 | 21 | result := composedFn(5) 22 | require.Equal(t, "6", result, "Unexpected result for Compose") 23 | 24 | result = composedFn(10) 25 | require.Equal(t, "11", result, "Unexpected result for Compose") 26 | } 27 | 28 | func TestMap(t *testing.T) { 29 | a := []int{1, 2, 3, 4, 5} 30 | 31 | fn := func(x int) string { 32 | return strconv.Itoa(x) 33 | } 34 | 35 | piper := Map(a, fn).Do() 36 | 37 | // Iterate through the values and test the output 38 | expected := []string{"1", "2", "3", "4", "5"} 39 | for i, val := range piper { 40 | require.Equal(t, expected[i], val, "Unexpected result for Map") 41 | } 42 | } 43 | 44 | func TestMapFilter(t *testing.T) { 45 | a := []int{1, 2, 3, 4, 5, 6, 7, 8} 46 | 47 | fn := func(x int) (string, bool) { 48 | return strconv.Itoa(x), x%2 == 0 49 | } 50 | 51 | piper := MapFilter(a, fn).Do() 52 | 53 | // Iterate through the values and test the output 54 | expected := []string{"2", "4", "6", "8"} 55 | for i, val := range piper { 56 | require.Equal(t, expected[i], val, "Unexpected result for MapFilter") 57 | } 58 | } 59 | 60 | func TestFilter(t *testing.T) { 61 | a := []int{1, 2, 3, 4, 5} 62 | 63 | fn := func(x *int) bool { 64 | return *x%2 != 0 65 | } 66 | 67 | piper := Filter(a, fn).Do() 68 | 69 | // Iterate through the values and test the output 70 | expected := []int{1, 3, 5} 71 | for i, val := range piper { 72 | require.Equal(t, expected[i], val, "Unexpected result for Filter") 73 | } 74 | } 75 | 76 | func TestReduce(t *testing.T) { 77 | a := []int{1, 2, 3, 4, 5} 78 | 79 | sum := func(result *int, x *int) int { 80 | return *result + *x 81 | } 82 | 83 | result := Reduce(a, sum) 84 | 85 | expected := 15 86 | require.Equal(t, expected, result, "Unexpected result for Reduce") 87 | } 88 | -------------------------------------------------------------------------------- /pkg/pipies/filters.go: -------------------------------------------------------------------------------- 1 | // Function set to use with Filter 2 | package pipies 3 | 4 | import ( 5 | "reflect" 6 | "sync" 7 | 8 | "golang.org/x/exp/constraints" 9 | 10 | "github.com/koss-null/funcfrog/pkg/pipe" 11 | ) 12 | 13 | // Predicates 14 | 15 | // NotNil returns true is x underlying value is not nil. 16 | // It uses reflection, so if you don't store any pointers, better use NotZero. 17 | func NotNil[T any](x *T) bool { 18 | return x != nil && !reflect.ValueOf(x).IsNil() 19 | } 20 | 21 | // IsNil returns true if x underlying value is nil. 22 | func IsNil[T any](x *T) bool { 23 | return x == nil || reflect.ValueOf(x).IsNil() 24 | } 25 | 26 | // NotZero returns true is x equals to a default zero value of the type T. 27 | func NotZero[T comparable](x *T) bool { 28 | var zero T 29 | return *x != zero 30 | } 31 | 32 | // Predicate builders 33 | 34 | // Eq returns a predicate which is true when the argument is equal to x. 35 | func Eq[T comparable](x T) pipe.Predicate[T] { 36 | return func(y *T) bool { 37 | return x == *y 38 | } 39 | } 40 | 41 | // NotEq returns a predicate which is true when the argument is NOT equal to x. 42 | func NotEq[T comparable](x T) pipe.Predicate[T] { 43 | return func(y *T) bool { 44 | return x != *y 45 | } 46 | } 47 | 48 | // LessThan returns a predicate which is true when the argument is less than x. 49 | func LessThan[T constraints.Ordered](x T) pipe.Predicate[T] { 50 | return func(y *T) bool { 51 | return *y < x 52 | } 53 | } 54 | 55 | // Distinct returns a predicate with filters out the same elements compated by the output of getKey function. 56 | // getKey function should receive an argument of a Pipe value type. 57 | // The result function is rather slow since it takes a lock on each element. 58 | // You should use Pipe.Distinct() to get better performance. 59 | func Distinct[T any, C comparable](getKey func(x *T) C) pipe.Predicate[T] { 60 | set := make(map[C]struct{}) 61 | var mx sync.Mutex 62 | 63 | return func(y *T) bool { 64 | key := getKey(y) 65 | 66 | mx.Lock() 67 | defer mx.Unlock() 68 | 69 | if _, ok := set[key]; ok { 70 | return false 71 | } 72 | set[key] = struct{}{} 73 | return true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/internalpipe/yeet_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestYeti_Yeet(t *testing.T) { 11 | t.Parallel() 12 | 13 | yeti := NewYeti() 14 | 15 | err := errors.New("sample error") 16 | yeti.Yeet(err) 17 | 18 | require.Contains(t, yeti.errs, err, "Error not added to Yeti's errors") 19 | 20 | err2 := errors.New("another error") 21 | yeti.Yeet(err2) 22 | 23 | require.Contains(t, yeti.errs, err2, "Error not added to Yeti's errors") 24 | } 25 | 26 | func TestYeti_Snag(t *testing.T) { 27 | t.Parallel() 28 | 29 | yeti := NewYeti() 30 | 31 | handler := func(err error) {} 32 | yeti.Snag(handler) 33 | 34 | require.Equal(t, 1, len(yeti.handlers), "Error handler not added to Yeti's handlers") 35 | } 36 | 37 | func TestYeti_Handle(t *testing.T) { 38 | t.Parallel() 39 | 40 | someErr := errors.New("some error") 41 | yeti := NewYeti() 42 | 43 | handlerCalled := false 44 | handler := func(err error) { 45 | require.ErrorIs(t, someErr, err) 46 | handlerCalled = true 47 | } 48 | 49 | yeti.Snag(handler) 50 | yeti.Yeet(someErr) 51 | yeti.Handle() 52 | 53 | require.True(t, handlerCalled, "Error handler not called for error") 54 | } 55 | 56 | func TestYeti_AddYeti(t *testing.T) { 57 | t.Parallel() 58 | 59 | yeti := NewYeti() 60 | yeti2 := NewYeti() 61 | yeti.AddYeti(yeti2) 62 | 63 | require.Contains(t, yeti.yetis, yeti2, "Yeti not added to Yeti's yetties") 64 | } 65 | 66 | func TestYeti_AddYetiHandle(t *testing.T) { 67 | t.Parallel() 68 | 69 | yeti := NewYeti() 70 | yeti2 := NewYeti() 71 | yeti.AddYeti(yeti2) 72 | 73 | someErr := errors.New("some error") 74 | someErr2 := errors.New("some error 2") 75 | 76 | handlerCalled := 0 77 | handler := func(err error) { 78 | require.ErrorIs(t, someErr, err) 79 | handlerCalled++ 80 | } 81 | handler2 := func(err error) { 82 | require.ErrorIs(t, someErr2, err) 83 | handlerCalled++ 84 | } 85 | 86 | yeti.Snag(handler) 87 | yeti2.Snag(handler2) 88 | yeti.Yeet(someErr) 89 | yeti.Yeet(someErr) 90 | yeti2.Yeet(someErr2) 91 | yeti.Handle() 92 | 93 | require.Equal(t, 3, handlerCalled, "Error handler is not called 3 times on 2 yeti") 94 | } 95 | -------------------------------------------------------------------------------- /internal/internalpipe/do.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | type ev[T any] struct { 10 | skipped bool 11 | obj *T 12 | } 13 | 14 | // Do evaluates all the pipeline and returns the result slice. 15 | func (p Pipe[T]) Do() []T { 16 | if p.limitSet() { 17 | res := p.doToLimit() 18 | return res 19 | } 20 | res, _ := p.do(true) 21 | return res 22 | } 23 | 24 | // doToLimit executor for Take 25 | func (p *Pipe[T]) doToLimit() []T { 26 | if p.ValLim == 0 { 27 | return []T{} 28 | } 29 | 30 | if p.y != nil { 31 | defer p.y.Handle() 32 | } 33 | 34 | res := make([]T, 0, p.ValLim) 35 | for i := 0; len(res) < p.ValLim; i++ { 36 | obj, skipped := p.Fn(i) 37 | if !skipped { 38 | res = append(res, *obj) 39 | } 40 | 41 | if i == math.MaxInt { 42 | panic(panicLimitExceededMsg) 43 | } 44 | } 45 | return res 46 | } 47 | 48 | // do runs the result evaluation. 49 | func (p *Pipe[T]) do(needResult bool) ([]T, int) { 50 | if p.y != nil { 51 | defer p.y.Handle() 52 | } 53 | 54 | var ( 55 | eval []ev[T] 56 | limit = p.limit() 57 | step = max(divUp(limit, p.GoroutinesCnt), 1) 58 | wg sync.WaitGroup 59 | skipCnt atomic.Int64 60 | ) 61 | if needResult && limit > 0 { 62 | eval = make([]ev[T], limit) 63 | } 64 | tickets := genTickets(p.GoroutinesCnt) 65 | for i := 0; i > -1 && i < limit; i += step { 66 | <-tickets 67 | wg.Add(1) 68 | go func(lf, rg int) { 69 | if rg < 0 { 70 | rg = limit 71 | } 72 | rg = min(rg, limit) 73 | var sCnt int64 74 | for j := lf; j < rg; j++ { 75 | obj, skipped := p.Fn(j) 76 | if skipped { 77 | sCnt++ 78 | } 79 | if needResult { 80 | eval[j] = ev[T]{ 81 | obj: obj, 82 | skipped: skipped, 83 | } 84 | } 85 | } 86 | skipCnt.Add(sCnt) 87 | tickets <- struct{}{} 88 | wg.Done() 89 | }(i, i+step) 90 | } 91 | wg.Wait() 92 | 93 | res := make([]T, 0, limit-int(skipCnt.Load())) 94 | for i := range eval { 95 | if !eval[i].skipped { 96 | res = append(res, *eval[i].obj) 97 | } 98 | } 99 | return res, limit - int(skipCnt.Load()) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/pipe/interface.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import "github.com/koss-null/funcfrog/internal/internalpipe" 4 | 5 | // Piper interface contains all methods of a pipe with determened length. 6 | type Piper[T any] interface { 7 | doer[T] 8 | 9 | mapper[T, Piper[T]] 10 | filterer[T, Piper[T]] 11 | sorter[T, Piper[T]] 12 | 13 | paralleller[T, Piper[T]] 14 | 15 | firster[T] 16 | anier[T] 17 | reducer[T] 18 | summer[T] 19 | counter 20 | 21 | promicer[T] 22 | eraser[Piper[any]] 23 | snagger[Piper[T]] 24 | yetyer[Piper[T]] 25 | } 26 | 27 | // PiperNoLen represents methods available to a Pipe type with no length determened. 28 | type PiperNoLen[T any] interface { 29 | taker[Piper[T]] 30 | genner[Piper[T]] 31 | 32 | mapper[T, PiperNoLen[T]] 33 | filterer[T, PiperNoLen[T]] 34 | 35 | paralleller[T, PiperNoLen[T]] 36 | 37 | firster[T] 38 | anier[T] 39 | 40 | eraser[PiperNoLen[any]] 41 | snagger[PiperNoLen[T]] 42 | yetyer[PiperNoLen[T]] 43 | } 44 | 45 | type paralleller[T, PiperT any] interface { 46 | Parallel(uint16) PiperT 47 | } 48 | 49 | type mapper[T, PiperT any] interface { 50 | Map(func(T) T) PiperT 51 | MapFilter(func(T) (T, bool)) PiperT 52 | } 53 | 54 | type filterer[T, PiperT any] interface { 55 | Filter(Predicate[T]) PiperT 56 | } 57 | 58 | type sorter[T, PiperT any] interface { 59 | Sort(Comparator[T]) PiperT 60 | } 61 | 62 | type reducer[T any] interface { 63 | Reduce(Accum[T]) *T 64 | } 65 | 66 | type summer[T any] interface { 67 | Sum(Accum[T]) T 68 | } 69 | 70 | type taker[T any] interface { 71 | Take(int) T 72 | } 73 | 74 | type genner[T any] interface { 75 | Gen(int) T 76 | } 77 | 78 | type doer[T any] interface { 79 | Do() []T 80 | } 81 | 82 | type firster[T any] interface { 83 | First() *T 84 | } 85 | 86 | type anier[T any] interface { 87 | Any() *T 88 | } 89 | 90 | type counter interface { 91 | Count() int 92 | } 93 | 94 | type eraser[PiperT any] interface { 95 | Erase() PiperT 96 | } 97 | 98 | type promicer[T any] interface { 99 | Promices() []func() (T, bool) 100 | } 101 | 102 | type snagger[PiperT any] interface { 103 | Snag(func(error)) PiperT 104 | } 105 | 106 | type yetyer[PiperT any] interface { 107 | Yeti(y internalpipe.YeetSnag) PiperT 108 | } 109 | -------------------------------------------------------------------------------- /internal/internalpipe/map_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Map(t *testing.T) { 10 | t.Parallel() 11 | 12 | exp := make([]int, 0, 100_000) 13 | for i := 0; i < 100_000; i++ { 14 | exp = append(exp, (((i+1)*2 + 55) / 2)) 15 | } 16 | 17 | t.Run("single thread lim set", func(t *testing.T) { 18 | p := Pipe[int]{ 19 | Fn: func(i int) (*int, bool) { 20 | return &i, false 21 | }, 22 | Len: 100_000, 23 | ValLim: -1, 24 | GoroutinesCnt: 1, 25 | } 26 | res := p.Map(func(x int) int { return x + 1 }). 27 | Map(func(x int) int { return x * 2 }). 28 | Map(func(x int) int { return (x + 55) / 2 }). 29 | Do() 30 | 31 | require.Equal(t, len(exp), len(res)) 32 | for i, r := range res { 33 | require.Equal(t, exp[i], r) 34 | } 35 | }) 36 | 37 | t.Run("seven thread lim set", func(t *testing.T) { 38 | p := Pipe[int]{ 39 | Fn: func(i int) (*int, bool) { 40 | return &i, false 41 | }, 42 | Len: 100_000, 43 | ValLim: -1, 44 | GoroutinesCnt: 7, 45 | } 46 | 47 | res := p.Map(func(x int) int { return x + 1 }). 48 | Map(func(x int) int { return x * 2 }). 49 | Map(func(x int) int { return (x + 55) / 2 }). 50 | Do() 51 | 52 | for i, r := range res { 53 | require.Equal(t, exp[i], r) 54 | } 55 | }) 56 | 57 | t.Run("single thread ValLim set", func(t *testing.T) { 58 | p := Pipe[int]{ 59 | Fn: func(i int) (*int, bool) { 60 | return &i, false 61 | }, 62 | Len: -1, 63 | ValLim: len(exp), 64 | GoroutinesCnt: 1, 65 | } 66 | res := p.Map(func(x int) int { return x + 1 }). 67 | Map(func(x int) int { return x * 2 }). 68 | Map(func(x int) int { return (x + 55) / 2 }). 69 | Do() 70 | 71 | require.Equal(t, len(exp), len(res)) 72 | for i, r := range res { 73 | require.Equal(t, exp[i], r) 74 | } 75 | }) 76 | 77 | t.Run("seven thread ValLim set", func(t *testing.T) { 78 | p := Pipe[int]{ 79 | Fn: func(i int) (*int, bool) { 80 | return &i, false 81 | }, 82 | Len: -1, 83 | ValLim: len(exp), 84 | GoroutinesCnt: 7, 85 | } 86 | res := p.Map(func(x int) int { return x + 1 }). 87 | Map(func(x int) int { return x * 2 }). 88 | Map(func(x int) int { return (x + 55) / 2 }). 89 | Do() 90 | 91 | require.Equal(t, len(exp), len(res)) 92 | for i, r := range res { 93 | require.Equal(t, exp[i], r) 94 | } 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /internal/internalpipe/do_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Do(t *testing.T) { 10 | t.Parallel() 11 | 12 | exp := make([]int, 0, 100_000) 13 | for i := 0; i < 100_000; i++ { 14 | if (i+1)%2 == 0 { 15 | exp = append(exp, (i+1)*2) 16 | } 17 | } 18 | 19 | t.Run("single thread lim set", func(t *testing.T) { 20 | p := Pipe[int]{ 21 | Fn: func(i int) (*int, bool) { 22 | return &i, false 23 | }, 24 | Len: 100_000, 25 | ValLim: -1, 26 | GoroutinesCnt: 1, 27 | } 28 | res := p.Map(func(x int) int { return x + 1 }). 29 | Filter(func(x *int) bool { return *x%2 == 0 }). 30 | Map(func(x int) int { return x * 2 }). 31 | Do() 32 | 33 | require.Equal(t, len(exp), len(res)) 34 | for i, r := range res { 35 | require.Equal(t, exp[i], r) 36 | } 37 | }) 38 | 39 | t.Run("seven thread lim set", func(t *testing.T) { 40 | p := Pipe[int]{ 41 | Fn: func(i int) (*int, bool) { 42 | return &i, false 43 | }, 44 | Len: 100_000, 45 | ValLim: -1, 46 | GoroutinesCnt: 7, 47 | } 48 | res := p.Map(func(x int) int { return x + 1 }). 49 | Filter(func(x *int) bool { return *x%2 == 0 }). 50 | Map(func(x int) int { return x * 2 }). 51 | Do() 52 | 53 | require.Equal(t, len(exp), len(res)) 54 | for i, r := range res { 55 | require.Equal(t, exp[i], r) 56 | } 57 | }) 58 | 59 | t.Run("single thread ValLim set", func(t *testing.T) { 60 | p := Pipe[int]{ 61 | Fn: func(i int) (*int, bool) { 62 | return &i, false 63 | }, 64 | Len: -1, 65 | ValLim: len(exp), 66 | GoroutinesCnt: 1, 67 | } 68 | res := p.Map(func(x int) int { return x + 1 }). 69 | Filter(func(x *int) bool { return *x%2 == 0 }). 70 | Map(func(x int) int { return x * 2 }). 71 | Do() 72 | 73 | require.Equal(t, len(exp), len(res)) 74 | for i, r := range res { 75 | require.Equal(t, exp[i], r) 76 | } 77 | }) 78 | 79 | t.Run("seven thread ValLim set", func(t *testing.T) { 80 | p := Pipe[int]{ 81 | Fn: func(i int) (*int, bool) { 82 | return &i, false 83 | }, 84 | Len: -1, 85 | ValLim: len(exp), 86 | GoroutinesCnt: 7, 87 | } 88 | res := p.Map(func(x int) int { return x + 1 }). 89 | Filter(func(x *int) bool { return *x%2 == 0 }). 90 | Map(func(x int) int { return x * 2 }). 91 | Do() 92 | 93 | require.Equal(t, len(exp), len(res)) 94 | for i, r := range res { 95 | require.Equal(t, exp[i], r) 96 | } 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /internal/algo/parallel/mergesort/mergesort_test.go: -------------------------------------------------------------------------------- 1 | package mergesort 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func _less(a, b int) bool { return a < b } 10 | 11 | func Test_merge(t *testing.T) { 12 | a := make([]int, 0, 200) 13 | lf, rg := 0, 0 14 | for i := 0; i < 200; i += 2 { 15 | rg++ 16 | a = append(a, i) 17 | } 18 | lf1, rg1 := rg, 200 19 | for i := 1; i < 200; i += 2 { 20 | a = append(a, i) 21 | } 22 | 23 | merge(a, lf, rg, lf1, rg1, _less) 24 | prev := -1 25 | for _, item := range a { 26 | require.GreaterOrEqual(t, item, prev) 27 | prev = item 28 | } 29 | } 30 | 31 | func Test_mergeSplits(t *testing.T) { 32 | a := make([]int, 0, 200) 33 | lf, rg := 0, 0 34 | for i := 0; i < 200; i += 2 { 35 | rg++ 36 | a = append(a, i) 37 | } 38 | lf1, rg1 := rg, 200 39 | for i := 1; i < 200; i += 2 { 40 | a = append(a, i) 41 | } 42 | 43 | mergeSplits(a, []border{{lf, rg}, {lf1, rg1}}, 3, _less) 44 | prev := -1 45 | for _, item := range a { 46 | require.GreaterOrEqual(t, item, prev) 47 | prev = item 48 | } 49 | } 50 | 51 | func Test_mergeSplits2(t *testing.T) { 52 | a := make([]int, 0, 300) 53 | lf, rg := 0, 100 54 | for i := 0; i < 300; i += 3 { 55 | a = append(a, i) 56 | } 57 | lf1, rg1 := 100, 200 58 | for i := 1; i < 300; i += 3 { 59 | a = append(a, i) 60 | } 61 | lf2, rg2 := 200, 300 62 | for i := 2; i < 300; i += 3 { 63 | a = append(a, i) 64 | } 65 | 66 | mergeSplits(a, []border{{lf, rg}, {lf1, rg1}, {lf2, rg2}}, 2, _less) 67 | prev := -1 68 | for _, item := range a { 69 | require.GreaterOrEqual(t, item, prev) 70 | prev = item 71 | } 72 | } 73 | 74 | func Test_Sort(t *testing.T) { 75 | a := make([]int, 0, 6000) 76 | for i := 0; i < 6000; i++ { 77 | a = append(a, 6000-i) 78 | } 79 | for i := 100; i < 1000; i++ { 80 | a[i] = i 81 | } 82 | res := Sort(a, _less, 12) 83 | 84 | prev := -1 85 | for _, item := range res { 86 | require.GreaterOrEqual(t, item, prev) 87 | prev = item 88 | } 89 | } 90 | 91 | func Test_Sort_Small(t *testing.T) { 92 | a := make([]int, 0, 3000) 93 | for i := 0; i < 3000; i++ { 94 | a = append(a, 3000-i) 95 | } 96 | for i := 100; i < 1000; i++ { 97 | a[i] = i 98 | } 99 | res := Sort(a, _less, 12) 100 | 101 | prev := -1 102 | for _, item := range res { 103 | require.GreaterOrEqual(t, item, prev) 104 | prev = item 105 | } 106 | } 107 | 108 | func Test_max(t *testing.T) { 109 | require.Equal(t, 5, max(1, 5)) 110 | require.Equal(t, 5, max(5, 5)) 111 | require.Equal(t, 5, max(5, 1)) 112 | } 113 | -------------------------------------------------------------------------------- /internal/internalpipe/pipe.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "math" 5 | 6 | "golang.org/x/exp/constraints" 7 | ) 8 | 9 | const ( 10 | panicLimitExceededMsg = "the limit have been exceeded, but the result is not calculated" 11 | ) 12 | 13 | type GeneratorFn[T any] func(int) (*T, bool) 14 | 15 | type Pipe[T any] struct { 16 | Fn GeneratorFn[T] 17 | Len int 18 | ValLim int 19 | GoroutinesCnt int 20 | 21 | y yeti 22 | } 23 | 24 | // Parallel set n - the amount of goroutines to run on. 25 | // Only the first Parallel() in a pipe chain is applied. 26 | func (p Pipe[T]) Parallel(n uint16) Pipe[T] { 27 | if p.GoroutinesCnt != defaultParallelWrks || n < 1 { 28 | return p 29 | } 30 | p.GoroutinesCnt = int(n) 31 | return p 32 | } 33 | 34 | // Take is used to set the amount of values expected to be in result slice. 35 | // It's applied only the first Gen() or Take() function in the pipe. 36 | func (p Pipe[T]) Take(n int) Pipe[T] { 37 | if p.limitSet() || p.lenSet() || n < 0 { 38 | return p 39 | } 40 | p.ValLim = n 41 | return p 42 | } 43 | 44 | // Gen set the amount of values to generate as initial array. 45 | // It's applied only the first Gen() or Take() function in the pipe. 46 | func (p Pipe[T]) Gen(n int) Pipe[T] { 47 | if p.limitSet() || p.lenSet() || n < 0 { 48 | return p 49 | } 50 | p.Len = n 51 | return p 52 | } 53 | 54 | // Count evaluates all the pipeline and returns the amount of items. 55 | func (p Pipe[T]) Count() int { 56 | if p.limitSet() { 57 | return p.ValLim 58 | } 59 | _, cnt := p.do(false) 60 | return cnt 61 | } 62 | 63 | // limit returns the upper border limit as the pipe evaluation limit. 64 | func (p *Pipe[T]) limit() int { 65 | switch { 66 | case p.lenSet(): 67 | return p.Len 68 | case p.limitSet(): 69 | return p.ValLim 70 | default: 71 | return math.MaxInt - 1 72 | } 73 | } 74 | 75 | func (p *Pipe[T]) lenSet() bool { 76 | return p.Len != notSet 77 | } 78 | 79 | func (p *Pipe[T]) limitSet() bool { 80 | return p.ValLim != notSet 81 | } 82 | 83 | func min[T constraints.Ordered](a, b T) T { 84 | if a > b { 85 | return b 86 | } 87 | return a 88 | } 89 | 90 | func max[T constraints.Ordered](a, b T) T { 91 | if a < b { 92 | return b 93 | } 94 | return a 95 | } 96 | 97 | func divUp(a, b int) int { 98 | return int(math.Ceil(float64(a) / float64(b))) 99 | } 100 | 101 | func genTickets(n int) chan struct{} { 102 | tickets := make(chan struct{}, n) 103 | n = max(n, 1) 104 | for i := 0; i < n; i++ { 105 | tickets <- struct{}{} 106 | } 107 | return tickets 108 | } 109 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= 8 | github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= 9 | github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= 10 | github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= 11 | github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= 12 | github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= 13 | github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 18 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 19 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 20 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 21 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 22 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 23 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /internal/internalpipe/filter_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/koss-null/funcfrog/internal/primitive/pointer" 9 | ) 10 | 11 | func Test_Filter(t *testing.T) { 12 | t.Parallel() 13 | 14 | t.Run("single thread even numbers filter", func(t *testing.T) { 15 | p := Pipe[int]{ 16 | Fn: func(i int) (*int, bool) { 17 | return &i, false 18 | }, 19 | Len: 100_000, 20 | ValLim: -1, 21 | GoroutinesCnt: 1, 22 | } 23 | 24 | res := p.Filter(func(x *int) bool { 25 | return pointer.Deref(x)%2 == 0 26 | }).Do() 27 | j := 0 28 | for i := 0; i < 100_000; i += 2 { 29 | require.Equal(t, i, res[j]) 30 | j++ 31 | } 32 | }) 33 | t.Run("seven thread even numbers filter", func(t *testing.T) { 34 | p := Pipe[int]{ 35 | Fn: func(i int) (*int, bool) { 36 | return &i, false 37 | }, 38 | Len: 100_000, 39 | ValLim: -1, 40 | GoroutinesCnt: 7, 41 | } 42 | 43 | res := p.Filter(func(x *int) bool { 44 | return pointer.Deref(x)%2 == 0 45 | }).Do() 46 | j := 0 47 | for i := 0; i < 100_000; i += 2 { 48 | require.Equal(t, i, res[j]) 49 | j++ 50 | } 51 | }) 52 | t.Run("single thread even numbers empty res filter", func(t *testing.T) { 53 | pts := pointer.Ref(7) 54 | p := Pipe[int]{ 55 | Fn: func(i int) (*int, bool) { 56 | return pts, false 57 | }, 58 | Len: 100_000, 59 | ValLim: -1, 60 | GoroutinesCnt: 1, 61 | } 62 | 63 | res := p.Filter(func(x *int) bool { 64 | return pointer.Deref(x)%2 == 0 65 | }).Do() 66 | require.Equal(t, 0, len(res)) 67 | }) 68 | t.Run("seven thread even numbers empty res filter", func(t *testing.T) { 69 | pts := pointer.Ref(7) 70 | p := Pipe[int]{ 71 | Fn: func(i int) (*int, bool) { 72 | return pts, false 73 | }, 74 | Len: 100_000, 75 | ValLim: -1, 76 | GoroutinesCnt: 7, 77 | } 78 | 79 | res := p.Filter(func(x *int) bool { 80 | return pointer.Deref(x)%2 == 0 81 | }).Do() 82 | require.Equal(t, 0, len(res)) 83 | }) 84 | t.Run("seven thread even numbers empty res double filter", func(t *testing.T) { 85 | pts := pointer.Ref(7) 86 | p := Pipe[int]{ 87 | Fn: func(i int) (*int, bool) { 88 | return pts, false 89 | }, 90 | Len: 100_000, 91 | ValLim: -1, 92 | GoroutinesCnt: 7, 93 | } 94 | 95 | res := p.Filter(func(x *int) bool { 96 | return pointer.Deref(x)%2 == 0 97 | }).Filter(func(x *int) bool { 98 | return pointer.Deref(x)%2 == 0 99 | }).Do() 100 | require.Equal(t, 0, len(res)) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /internal/internalpipe/first_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func genSlice(n int) []int { 10 | res := make([]int, n) 11 | for i := 0; i < n; i++ { 12 | res[i] = i 13 | } 14 | return res 15 | } 16 | 17 | func TestFirst(t *testing.T) { 18 | s := genSlice(1_000_000) 19 | t.Parallel() 20 | 21 | t.Run("single thread", func(t *testing.T) { 22 | t.Parallel() 23 | 24 | p := Pipe[int]{ 25 | Fn: func(i int) (*int, bool) { 26 | return &s[i], i <= 900_000 27 | }, 28 | Len: len(s), 29 | ValLim: -1, 30 | GoroutinesCnt: 1, 31 | } 32 | require.Equal(t, 900_001, *p.First()) 33 | }) 34 | 35 | t.Run("single thread 2", func(t *testing.T) { 36 | t.Parallel() 37 | 38 | p := Pipe[int]{ 39 | Fn: func(i int) (*int, bool) { 40 | return &s[i], true 41 | }, 42 | Len: len(s), 43 | ValLim: -1, 44 | GoroutinesCnt: 1, 45 | } 46 | require.Nil(t, p.First()) 47 | }) 48 | 49 | t.Run("5 threads", func(t *testing.T) { 50 | t.Parallel() 51 | 52 | p := Pipe[int]{ 53 | Fn: func(i int) (*int, bool) { 54 | return &s[i], i <= 900_000 55 | }, 56 | Len: len(s), 57 | ValLim: -1, 58 | GoroutinesCnt: 5, 59 | } 60 | require.NotNil(t, p.First()) 61 | require.Equal(t, 900_001, *p.First()) 62 | }) 63 | 64 | t.Run("2 threads first half", func(t *testing.T) { 65 | t.Parallel() 66 | 67 | p := Pipe[int]{ 68 | Fn: func(i int) (*int, bool) { 69 | return &s[i], s[i] < 1000 70 | }, 71 | Len: len(s), 72 | ValLim: -1, 73 | GoroutinesCnt: 2, 74 | } 75 | require.NotNil(t, p.First()) 76 | require.Equal(t, 1000, *p.First()) 77 | }) 78 | 79 | t.Run("1000 threads", func(t *testing.T) { 80 | t.Parallel() 81 | 82 | p := Pipe[int]{ 83 | Fn: func(i int) (*int, bool) { 84 | return &s[i], i <= 900_000 85 | }, 86 | Len: len(s), 87 | ValLim: -1, 88 | GoroutinesCnt: 1000, 89 | } 90 | require.NotNil(t, p.First()) 91 | require.Equal(t, 900_001, *p.First()) 92 | }) 93 | 94 | t.Run("not found", func(t *testing.T) { 95 | t.Parallel() 96 | 97 | p := Pipe[int]{ 98 | Fn: func(i int) (*int, bool) { 99 | return nil, true 100 | }, 101 | Len: len(s), 102 | ValLim: -1, 103 | GoroutinesCnt: 10, 104 | } 105 | require.Nil(t, p.First()) 106 | }) 107 | 108 | t.Run("not found len 0", func(t *testing.T) { 109 | t.Parallel() 110 | 111 | p := Pipe[int]{ 112 | Fn: func(i int) (*int, bool) { 113 | return nil, true 114 | }, 115 | Len: 0, 116 | ValLim: 0, 117 | GoroutinesCnt: 10, 118 | } 119 | require.Nil(t, p.First()) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /internal/internalpipe/constructor.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "math" 5 | 6 | "golang.org/x/exp/constraints" 7 | ) 8 | 9 | const ( 10 | defaultParallelWrks = 1 11 | notSet = -1 12 | ) 13 | 14 | func Slice[T any](dt []T) Pipe[T] { 15 | return Pipe[T]{ 16 | Fn: func(i int) (*T, bool) { 17 | if i >= len(dt) { 18 | return nil, true 19 | } 20 | return &dt[i], false 21 | }, 22 | Len: len(dt), 23 | ValLim: notSet, 24 | GoroutinesCnt: defaultParallelWrks, 25 | } 26 | } 27 | 28 | func Func[T any](fn func(i int) (T, bool)) Pipe[T] { 29 | return Pipe[T]{ 30 | Fn: func(i int) (*T, bool) { 31 | obj, exist := fn(i) 32 | return &obj, !exist 33 | }, 34 | Len: notSet, 35 | ValLim: notSet, 36 | GoroutinesCnt: defaultParallelWrks, 37 | } 38 | } 39 | 40 | func FuncP[T any](fn func(i int) (*T, bool)) Pipe[T] { 41 | return Pipe[T]{ 42 | Fn: func(i int) (*T, bool) { 43 | obj, exist := fn(i) 44 | return obj, !exist 45 | }, 46 | Len: notSet, 47 | ValLim: notSet, 48 | GoroutinesCnt: defaultParallelWrks, 49 | } 50 | } 51 | 52 | func Cycle[T any](a []T) Pipe[T] { 53 | if len(a) == 0 { 54 | return Func(func(_ int) (x T, exist bool) { 55 | return 56 | }) 57 | } 58 | 59 | return Func(func(i int) (T, bool) { 60 | return a[i%len(a)], true 61 | }) 62 | } 63 | 64 | func Range[T constraints.Integer | constraints.Float](start, finish, step T) Pipe[T] { 65 | if (step == 0) || 66 | (step > 0 && start >= finish) || 67 | (step < 0 && finish >= start) { 68 | return Pipe[T]{ 69 | Fn: nil, 70 | Len: 0, 71 | ValLim: notSet, 72 | GoroutinesCnt: defaultParallelWrks, 73 | } 74 | } 75 | 76 | pred := func(x T) bool { 77 | return x >= finish 78 | } 79 | if step < 0 { 80 | pred = func(x T) bool { 81 | return x <= finish 82 | } 83 | } 84 | return Pipe[T]{ 85 | Fn: func(i int) (*T, bool) { 86 | val := start + T(i)*step 87 | return &val, pred(val) 88 | }, 89 | Len: ceil(float64(finish-start) / float64(step)), 90 | ValLim: notSet, 91 | GoroutinesCnt: defaultParallelWrks, 92 | } 93 | } 94 | 95 | func Repeat[T any](x T, n int) Pipe[T] { 96 | if n <= 0 { 97 | return Pipe[T]{ 98 | Fn: nil, 99 | Len: 0, 100 | ValLim: notSet, 101 | GoroutinesCnt: defaultParallelWrks, 102 | } 103 | } 104 | 105 | return Pipe[T]{ 106 | Fn: func(i int) (*T, bool) { 107 | cp := x 108 | return &cp, i >= n 109 | }, 110 | Len: n, 111 | ValLim: notSet, 112 | GoroutinesCnt: defaultParallelWrks, 113 | } 114 | } 115 | 116 | func ceil[T constraints.Integer | constraints.Float](a T) int { 117 | return int(math.Ceil(float64(a))) 118 | } 119 | -------------------------------------------------------------------------------- /internal/internalpipe/sum_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/koss-null/funcfrog/internal/primitive/pointer" 9 | ) 10 | 11 | func Test_Sum(t *testing.T) { 12 | initA100k() 13 | t.Parallel() 14 | 15 | t.Run("Single thread happy 100k sum", func(t *testing.T) { 16 | p := Pipe[float64]{ 17 | Fn: func(i int) (*float64, bool) { 18 | return &a100k[i], false 19 | }, 20 | Len: len(a100k), 21 | ValLim: -1, 22 | GoroutinesCnt: 1, 23 | } 24 | s := p.Sum(func(x, y *float64) float64 { 25 | return *x + *y 26 | }) 27 | 28 | require.NotNil(t, s) 29 | require.Equal(t, 4999950000.0, s) 30 | }) 31 | 32 | t.Run("Quadro thread happy 100k sum", func(t *testing.T) { 33 | p := Pipe[float64]{ 34 | Fn: func(i int) (*float64, bool) { 35 | return &a100k[i], false 36 | }, 37 | Len: len(a100k), 38 | ValLim: -1, 39 | GoroutinesCnt: 4, 40 | } 41 | s := p.Sum(func(x, y *float64) float64 { 42 | return *x + *y 43 | }) 44 | 45 | require.NotNil(t, s) 46 | require.Equal(t, 4999950000.0, s) 47 | }) 48 | 49 | t.Run("Single thread empty", func(t *testing.T) { 50 | p := Pipe[float64]{ 51 | Fn: func(i int) (*float64, bool) { 52 | return pointer.Ref(1.), false 53 | }, 54 | Len: 0, 55 | ValLim: -1, 56 | GoroutinesCnt: 1, 57 | } 58 | s := p.Sum(func(x, y *float64) float64 { 59 | return *x + *y 60 | }) 61 | 62 | require.NotNil(t, s) 63 | require.Equal(t, 0.0, s) 64 | }) 65 | 66 | t.Run("Quadro thread empty", func(t *testing.T) { 67 | p := Pipe[float64]{ 68 | Fn: func(i int) (*float64, bool) { 69 | return pointer.Ref(1.), false 70 | }, 71 | Len: 0, 72 | ValLim: -1, 73 | GoroutinesCnt: 4, 74 | } 75 | s := p.Sum(func(x, y *float64) float64 { 76 | return *x + *y 77 | }) 78 | 79 | require.NotNil(t, s) 80 | require.Equal(t, 0.0, s) 81 | }) 82 | 83 | t.Run("Single thread 1 elem", func(t *testing.T) { 84 | p := Pipe[float64]{ 85 | Fn: func(i int) (*float64, bool) { 86 | return pointer.Ref(100500.), i != 0 87 | }, 88 | Len: 1, 89 | ValLim: -1, 90 | GoroutinesCnt: 1, 91 | } 92 | s := p.Sum(func(x, y *float64) float64 { 93 | return *x + *y 94 | }) 95 | 96 | require.NotNil(t, s) 97 | require.Equal(t, 100500., s) 98 | }) 99 | 100 | t.Run("Quadro thread 1 elem", func(t *testing.T) { 101 | p := Pipe[float64]{ 102 | Fn: func(i int) (*float64, bool) { 103 | return pointer.Ref(100500.), i != 0 104 | }, 105 | Len: 1, 106 | ValLim: -1, 107 | GoroutinesCnt: 4, 108 | } 109 | s := p.Sum(func(x, y *float64) float64 { 110 | return *x + *y 111 | }) 112 | 113 | require.NotNil(t, s) 114 | require.Equal(t, 100500., s) 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /internal/internalpipe/any.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | const hugeLenStep = 1 << 15 9 | 10 | func anySingleThread[T any](limit int, fn GeneratorFn[T]) *T { 11 | var obj *T 12 | var skipped bool 13 | 14 | for i := 0; i < limit; i++ { 15 | if obj, skipped = fn(i); !skipped { 16 | return obj 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // Any returns a pointer to a random element in the pipe or nil if none left. 23 | func (p Pipe[T]) Any() *T { 24 | const mutexUpdateCoef = 18 25 | 26 | limit := p.limit() 27 | if p.GoroutinesCnt == 1 { 28 | return anySingleThread(limit, p.Fn) 29 | } 30 | 31 | lenSet := p.lenSet() 32 | step := hugeLenStep 33 | if lenSet { 34 | step = max(divUp(limit, p.GoroutinesCnt), 1) 35 | } 36 | 37 | var ( 38 | resSet bool 39 | resCh = make(chan *T, 1) 40 | mx sync.Mutex 41 | 42 | tickets = genTickets(p.GoroutinesCnt) 43 | wg sync.WaitGroup 44 | ) 45 | defer close(resCh) 46 | setObj := func(obj *T) { 47 | mx.Lock() 48 | if !resSet { 49 | resSet = true 50 | resCh <- obj 51 | } 52 | mx.Unlock() 53 | } 54 | 55 | go func() { 56 | // i >= 0 is for an int owerflow case 57 | for i := 0; i >= 0 && (!lenSet || i < limit); i += step { 58 | wg.Add(1) 59 | <-tickets 60 | 61 | go func(lf, rg int) { 62 | defer func() { 63 | tickets <- struct{}{} 64 | wg.Done() 65 | }() 66 | // int owerflow case 67 | rg = max(rg, 0) 68 | if lenSet { 69 | rg = min(rg, limit) 70 | } 71 | 72 | var avgFnTime time.Duration 73 | var avgUpdResSetTime time.Duration 74 | resSetUpdCnt := int64(0) 75 | beforeLastResSetUpd := 0 76 | 77 | getResSet := func() bool { 78 | start := time.Now() 79 | mx.Lock() 80 | rs := resSet 81 | mx.Unlock() 82 | avgUpdResSetTime = time.Duration( 83 | (int64(time.Since(start)) + int64(avgUpdResSetTime)*(resSetUpdCnt)) / (resSetUpdCnt + 1), 84 | ) 85 | resSetUpdCnt++ 86 | beforeLastResSetUpd = 0 87 | return rs 88 | } 89 | rs := getResSet() 90 | cnt := 0 91 | for j := lf; j < rg; j++ { 92 | beforeLastResSetUpd++ 93 | if j != lf && 94 | avgFnTime != 0 && 95 | int64(beforeLastResSetUpd) > (mutexUpdateCoef*int64(avgUpdResSetTime)/int64(avgFnTime)) { 96 | rs = getResSet() 97 | cnt++ 98 | } 99 | if !rs { 100 | start := time.Now() 101 | obj, skipped := p.Fn(j) 102 | if !skipped { 103 | setObj(obj) 104 | return 105 | } 106 | avgFnTime = time.Duration( 107 | (int64(time.Since(start)) + int64(avgFnTime)*int64(j-lf)) / int64(j-lf+1), 108 | ) 109 | } 110 | } 111 | }(i, i+step) 112 | } 113 | 114 | go func() { 115 | wg.Wait() 116 | setObj(nil) 117 | defer close(tickets) 118 | }() 119 | }() 120 | 121 | return <-resCh 122 | } 123 | -------------------------------------------------------------------------------- /pkg/pipe/pipenl.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "github.com/koss-null/funcfrog/internal/internalpipe" 5 | ) 6 | 7 | // PipeNL implements the pipe on any slice. 8 | // PipeNL may be initialized with `Slice`, `Func`, `Cycle` or `Range`. 9 | type PipeNL[T any] struct { 10 | internalpipe.Pipe[T] 11 | } 12 | 13 | // Map applies given function to each element of the underlying slice 14 | // returns the slice where each element is n[i] = f(p[i]). 15 | func (p *PipeNL[T]) Map(fn func(T) T) PiperNoLen[T] { 16 | return &PipeNL[T]{p.Pipe.Map(fn)} 17 | } 18 | 19 | // Filter leaves only items with true predicate fn. 20 | func (p *PipeNL[T]) Filter(fn Predicate[T]) PiperNoLen[T] { 21 | return &PipeNL[T]{p.Pipe.Filter(fn)} 22 | } 23 | 24 | // MapFilter applies given function to each element of the underlying slice, 25 | // if the second returning value of fn is false, the element is skipped (may be useful for error handling). 26 | // returns the slice where each element is n[i] = f(p[i]) if it is not skipped. 27 | func (p *PipeNL[T]) MapFilter(fn func(T) (T, bool)) PiperNoLen[T] { 28 | return &PipeNL[T]{p.Pipe.MapFilter(fn)} 29 | } 30 | 31 | // First returns the first element of the pipe. 32 | func (p *PipeNL[T]) First() *T { 33 | return p.Pipe.First() 34 | } 35 | 36 | // Any returns a pointer to a random element in the pipe or nil if none left. 37 | func (p *PipeNL[T]) Any() *T { 38 | return p.Pipe.Any() 39 | } 40 | 41 | // Take is used to set the amount of values expected to be in result slice. 42 | // It's applied only the first Gen() or Take() function in the pipe. 43 | func (p *PipeNL[T]) Take(n int) Piper[T] { 44 | return &Pipe[T]{p.Pipe.Take(n)} 45 | } 46 | 47 | // Gen set the amount of values to generate as initial array. 48 | // It's applied only the first Gen() or Take() function in the pipe. 49 | func (p *PipeNL[T]) Gen(n int) Piper[T] { 50 | return &Pipe[T]{p.Pipe.Gen(n)} 51 | } 52 | 53 | // Parallel set n - the amount of goroutines to run on. 54 | // Only the first Parallel() in a pipe chain is applied. 55 | func (p *PipeNL[T]) Parallel(n uint16) PiperNoLen[T] { 56 | return &PipeNL[T]{p.Pipe.Parallel(n)} 57 | } 58 | 59 | // Erase wraps all pipe values to interface{} type, so you are able to use pipe methods with type convertions. 60 | // You can use collectors from collectiors.go file of this package to collect results into a particular type. 61 | func (p *PipeNL[T]) Erase() PiperNoLen[any] { 62 | return &PipeNL[any]{p.Pipe.Erase()} 63 | } 64 | 65 | // Snag links an error handler to the previous Pipe method. 66 | func (p *PipeNL[T]) Snag(h func(error)) PiperNoLen[T] { 67 | return &PipeNL[T]{p.Pipe.Snag(internalpipe.ErrHandler(h))} 68 | } 69 | 70 | // Yeti links a yeti error handler to the Pipe. 71 | func (p *PipeNL[T]) Yeti(y internalpipe.YeetSnag) PiperNoLen[T] { 72 | return &PipeNL[T]{p.Pipe.Yeti(y)} 73 | } 74 | 75 | // Entrails is an out of Piper interface method to provide Map[T1 -> T2]. 76 | func (p *PipeNL[T]) Entrails() *internalpipe.Pipe[T] { 77 | return &p.Pipe 78 | } 79 | -------------------------------------------------------------------------------- /pkg/pipe/constructors.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "golang.org/x/exp/constraints" 5 | 6 | "github.com/koss-null/funcfrog/internal/internalpipe" 7 | ) 8 | 9 | // Slice creates a Pipe from a slice 10 | func Slice[T any](dt []T) Piper[T] { 11 | return &Pipe[T]{internalpipe.Slice(dt)} 12 | } 13 | 14 | // Func creates a lazy sequence by applying the provided function 'fn' 15 | // to each index 'i', placing the resulting object into the sequence at index 'i'. 16 | // 17 | // The 'fn' function must return an object of type T, along with a boolean indicating 18 | // if it exists. 19 | // 20 | // Use the 'Take' or 'Gen' functions to set the number of output values to generate, 21 | // or use the 'Until' function to enforce a limit based on a predicate function. 22 | func Func[T any](fn func(i int) (T, bool)) PiperNoLen[T] { 23 | return &PipeNL[T]{internalpipe.Func(fn)} 24 | } 25 | 26 | // Fu creates a lazy sequence by applying the provided function 'fn' to each index 'i', 27 | // and setting the result to the sequence at index 'i'. 28 | // 29 | // 'fn' is a shortened version of the 'Func' function, where the second argument is true by default. 30 | // 31 | // Use the 'Take' or 'Gen' functions to set the number of output values to generate, 32 | // or use the 'Until' function to enforce a limit based on a predicate function. 33 | func Fn[T any](fn func(i int) T) PiperNoLen[T] { 34 | return Func(func(i int) (T, bool) { 35 | return fn(i), true 36 | }) 37 | } 38 | 39 | // FuncP is the same as Func but allows to return pointers to the values. 40 | // It creates a lazy sequence by applying the provided function 'fn' to each index 'i', and 41 | // setting the result to the sequence at index 'i' as a pointer to the object of type 'T'. 42 | // 43 | // 'fn' should return a pointer to an object of type 'T', along with a boolean indicating 44 | // whether the object exists. 45 | // 46 | // Use the 'Take' or 'Gen' functions to set the number of output values to generate, 47 | // or use the 'Until' function to enforce a limit based on a predicate function. 48 | func FuncP[T any](fn func(i int) (*T, bool)) PiperNoLen[T] { 49 | return &PipeNL[T]{internalpipe.FuncP(fn)} 50 | } 51 | 52 | // Cycle creates a lazy sequence that cycles through the elements of the provided slice 'a'. 53 | // To use the resulting sequence, the 'Cycle' function returns a 'PiperNoLen' object. 54 | // 55 | // Use the 'Take' or 'Gen' functions to set the number of output values to generate, 56 | // or use the 'Until' function to enforce a limit based on a predicate function. 57 | func Cycle[T any](a []T) PiperNoLen[T] { 58 | return &PipeNL[T]{internalpipe.Cycle(a)} 59 | } 60 | 61 | // Range creates a lazy sequence of type 'T', which consists of values 62 | // starting from 'start', incrementing by 'step' and stopping just before 'finish', i.e. [start..finish). 63 | // The type 'T' can be either an integer or a float. 64 | func Range[T constraints.Integer | constraints.Float](start, finish, step T) Piper[T] { 65 | return &Pipe[T]{internalpipe.Range(start, finish, step)} 66 | } 67 | 68 | // Repeat creates a lazy sequence of the objects like x with the length of n. 69 | func Repeat[T any](x T, n int) Piper[T] { 70 | return &Pipe[T]{internalpipe.Repeat(x, n)} 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/coverage-badge.yml: -------------------------------------------------------------------------------- 1 | name: Coverage badge 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | env: 9 | COVERAGE_FILE: coverage.out 10 | BADGE_FILE: coverage.svg 11 | 12 | jobs: 13 | test-and-badge: 14 | if: github.actor != 'github-actions[bot]' 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Skip if commit message requests it 24 | run: | 25 | if git log -1 --pretty=%B | grep -q '$$skip badge$$'; then 26 | echo "Found [skip badge] in commit message — exiting" 27 | exit 0 28 | fi 29 | 30 | - name: Set up Go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: '1.21' 34 | 35 | - name: Run tests and produce coverage 36 | run: | 37 | set -euo pipefail 38 | rm -f $COVERAGE_FILE 39 | # Run tests for all packages and create/merge coverage profile 40 | go test ./... -covermode=count -coverprofile=profile.tmp || true 41 | if [ -f profile.tmp ]; then 42 | # Process coverage file 43 | echo "mode: count" > $COVERAGE_FILE 44 | tail -n +2 profile.tmp >> $COVERAGE_FILE 45 | rm -f profile.tmp 46 | else 47 | echo "mode: count" > $COVERAGE_FILE 48 | fi 49 | 50 | - name: Generate coverage badge using shields.io 51 | run: | 52 | # Calculate coverage percentage 53 | COVERAGE_PERCENT=$(go tool cover -func=$COVERAGE_FILE | grep total | awk '{print $3}' | sed 's/%//') 54 | echo "Coverage: $COVERAGE_PERCENT%" 55 | 56 | # Determine badge color based on coverage 57 | if (( $(echo "$COVERAGE_PERCENT < 50" | bc -l) )); then 58 | COLOR=red 59 | elif (( $(echo "$COVERAGE_PERCENT < 70" | bc -l) )); then 60 | COLOR=yellow 61 | elif (( $(echo "$COVERAGE_PERCENT < 80" | bc -l) )); then 62 | COLOR=orange 63 | elif (( $(echo "$COVERAGE_PERCENT < 90" | bc -l) )); then 64 | COLOR=green 65 | else 66 | COLOR=brightgreen 67 | fi 68 | 69 | rm -f $COVERAGE_FILE 70 | # Download badge from shields.io 71 | curl -s "https://img.shields.io/badge/coverage-${COVERAGE_PERCENT}%25-${COLOR}.svg" -o $BADGE_FILE 72 | echo "Generated badge with $COVERAGE_PERCENT% coverage ($COLOR)" 73 | 74 | - name: Commit and push coverage badge 75 | if: github.ref == 'refs/heads/master' 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | run: | 79 | git config user.name "github-actions[bot]" 80 | git config user.email "github-actions[bot]@users.noreply.github.com" 81 | git add $BADGE_FILE README.md || true 82 | if git diff --staged --quiet; then 83 | echo "No badge changes to commit" 84 | exit 0 85 | fi 86 | git commit -m "chore: update coverage badge [skip badge]" 87 | git push origin HEAD:master 88 | -------------------------------------------------------------------------------- /internal/internalpipe/reduce_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Reduce(t *testing.T) { 10 | t.Parallel() 11 | 12 | t.Run("single thread lim set", func(t *testing.T) { 13 | p := Pipe[int]{ 14 | Fn: func(i int) (*int, bool) { 15 | return &i, false 16 | }, 17 | Len: 100_000, 18 | ValLim: -1, 19 | GoroutinesCnt: 1, 20 | } 21 | res := p.Reduce(func(x, y *int) int { return *x + *y }) 22 | require.Equal(t, 4999950000, *res) 23 | }) 24 | 25 | t.Run("seven thread lim set", func(t *testing.T) { 26 | p := Pipe[int]{ 27 | Fn: func(i int) (*int, bool) { 28 | return &i, false 29 | }, 30 | Len: 100_000, 31 | ValLim: -1, 32 | GoroutinesCnt: 7, 33 | } 34 | res := p.Reduce(func(x, y *int) int { return *x + *y }) 35 | require.Equal(t, 4999950000, *res) 36 | }) 37 | 38 | t.Run("single thread ValLim set", func(t *testing.T) { 39 | p := Pipe[int]{ 40 | Fn: func(i int) (*int, bool) { 41 | return &i, false 42 | }, 43 | Len: -1, 44 | ValLim: 100_000, 45 | GoroutinesCnt: 1, 46 | } 47 | res := p.Reduce(func(x, y *int) int { return *x + *y }) 48 | require.Equal(t, 4999950000, *res) 49 | }) 50 | 51 | t.Run("seven thread ValLim set", func(t *testing.T) { 52 | p := Pipe[int]{ 53 | Fn: func(i int) (*int, bool) { 54 | return &i, false 55 | }, 56 | Len: -1, 57 | ValLim: 100_000, 58 | GoroutinesCnt: 7, 59 | } 60 | 61 | res := p.Reduce(func(x, y *int) int { return *x + *y }) 62 | require.Equal(t, 4999950000, *res) 63 | }) 64 | 65 | t.Run("single thread single value", func(t *testing.T) { 66 | p := Pipe[int]{ 67 | Fn: func(i int) (*int, bool) { 68 | i++ 69 | return &i, false 70 | }, 71 | Len: -1, 72 | ValLim: 1, 73 | GoroutinesCnt: 1, 74 | } 75 | 76 | res := p.Reduce(func(x, y *int) int { return *x + *y }) 77 | require.Equal(t, 1, *res) 78 | }) 79 | 80 | t.Run("single thread zero value", func(t *testing.T) { 81 | p := Pipe[int]{ 82 | Fn: func(i int) (*int, bool) { 83 | return &i, false 84 | }, 85 | Len: -1, 86 | ValLim: 0, 87 | GoroutinesCnt: 1, 88 | } 89 | 90 | res := p.Reduce(func(x, y *int) int { return *x + *y }) 91 | require.Nil(t, res) 92 | }) 93 | 94 | t.Run("seven thread single value", func(t *testing.T) { 95 | p := Pipe[int]{ 96 | Fn: func(i int) (*int, bool) { 97 | i++ 98 | return &i, false 99 | }, 100 | Len: -1, 101 | ValLim: 1, 102 | GoroutinesCnt: 7, 103 | } 104 | 105 | res := p.Reduce(func(x, y *int) int { return *x + *y }) 106 | require.Equal(t, 1, *res) 107 | }) 108 | 109 | t.Run("seven thread zero value", func(t *testing.T) { 110 | p := Pipe[int]{ 111 | Fn: func(i int) (*int, bool) { 112 | return &i, false 113 | }, 114 | Len: -1, 115 | ValLim: 0, 116 | GoroutinesCnt: 7, 117 | } 118 | 119 | res := p.Reduce(func(x, y *int) int { return *x + *y }) 120 | require.Nil(t, res) 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /internal/algo/parallel/qsort/qsort.go: -------------------------------------------------------------------------------- 1 | package qsort 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | ) 7 | 8 | const ( 9 | singleThreadSortTreshold = 5000 10 | ) 11 | 12 | func Sort[T any](data []T, less func(*T, *T) bool, threads int) []T { 13 | if len(data) < 2 { 14 | return data 15 | } 16 | if threads < 1 { 17 | threads = 1 18 | } 19 | if threads == 1 { 20 | sort.Slice(data, func(i, j int) bool { 21 | return less(&data[i], &data[j]) 22 | }) 23 | return data 24 | } 25 | 26 | var wg sync.WaitGroup 27 | wg.Add(1) 28 | qsort(data, 0, len(data)-1, less, genTickets(threads), &wg) 29 | wg.Wait() 30 | return data 31 | } 32 | 33 | func qsort[T any]( 34 | data []T, 35 | lf, rg int, 36 | less func(*T, *T) bool, 37 | tickets chan struct{}, 38 | wg *sync.WaitGroup, 39 | ) { 40 | defer wg.Done() 41 | if lf >= rg { 42 | return 43 | } 44 | 45 | <-tickets 46 | defer func() { tickets <- struct{}{} }() 47 | 48 | if rg-lf < singleThreadSortTreshold { 49 | sort.Slice(data[lf:rg+1], func(i, j int) bool { 50 | return less(&data[lf+i], &data[lf+j]) 51 | }) 52 | 53 | return 54 | } 55 | 56 | q := partition(data, lf, rg, less) 57 | wg.Add(2) 58 | go qsort(data, lf, q, less, tickets, wg) 59 | go qsort(data, q+1, rg, less, tickets, wg) 60 | } 61 | 62 | func partition[T any](data []T, lf, rg int, less func(*T, *T) bool) int { 63 | // small arrays are not sorted with this method 64 | midIdx := (rg-lf)/2 + lf 65 | med, medIdx := median([3]T{data[lf], data[midIdx], data[rg]}, less) 66 | switch medIdx { 67 | case 0: 68 | data[lf], data[midIdx] = data[midIdx], data[lf] 69 | case 2: 70 | data[rg], data[midIdx] = data[midIdx], data[rg] 71 | } 72 | 73 | for lf <= rg { 74 | for less(&data[lf], med) { 75 | lf++ 76 | } 77 | for less(med, &data[rg]) { 78 | rg-- 79 | } 80 | if lf >= rg { 81 | break 82 | } 83 | data[lf], data[rg] = data[rg], data[lf] 84 | lf++ 85 | rg-- 86 | } 87 | 88 | return rg 89 | } 90 | 91 | // median returns the median of 3 elements; it's a bit ugly bit effective 92 | func median[T any](elems [3]T, less func(*T, *T) bool) (*T, int16) { 93 | if less(&elems[1], &elems[0]) && less(&elems[0], &elems[2]) { 94 | return &elems[0], 0 95 | } 96 | if less(&elems[2], &elems[0]) && less(&elems[0], &elems[1]) { 97 | return &elems[0], 0 98 | } 99 | 100 | if less(&elems[0], &elems[1]) && less(&elems[1], &elems[2]) { 101 | return &elems[1], 1 102 | } 103 | if less(&elems[2], &elems[1]) && less(&elems[1], &elems[0]) { 104 | return &elems[1], 1 105 | } 106 | 107 | if less(&elems[0], &elems[2]) && less(&elems[2], &elems[1]) { 108 | return &elems[2], 2 109 | } 110 | if less(&elems[1], &elems[2]) && less(&elems[2], &elems[0]) { 111 | return &elems[2], 2 112 | } 113 | 114 | // two elements are equal 115 | if !less(&elems[0], &elems[1]) && !less(&elems[1], &elems[0]) { 116 | return &elems[0], 0 117 | } 118 | if !less(&elems[1], &elems[2]) && !less(&elems[2], &elems[1]) { 119 | return &elems[1], 1 120 | } 121 | return &elems[2], 2 122 | } 123 | 124 | func genTickets(n int) chan struct{} { 125 | tickets := make(chan struct{}, n) 126 | for i := 0; i < n; i++ { 127 | tickets <- struct{}{} 128 | } 129 | return tickets 130 | } 131 | -------------------------------------------------------------------------------- /internal/internalpipe/first.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "sync" 7 | ) 8 | 9 | func (p Pipe[T]) First() *T { 10 | limit := p.limit() 11 | if p.GoroutinesCnt == 1 { 12 | return firstSingleThread(limit, p.Fn) 13 | } 14 | return first(limit, p.GoroutinesCnt, p.Fn) 15 | } 16 | 17 | func firstSingleThread[T any](limit int, fn func(i int) (*T, bool)) *T { 18 | var obj *T 19 | var skipped bool 20 | for i := 0; i < limit; i++ { 21 | obj, skipped = fn(i) 22 | if !skipped { 23 | return obj 24 | } 25 | } 26 | return nil 27 | } 28 | 29 | type firstResult[T any] struct { 30 | val *T 31 | step int 32 | totalSteps int 33 | zeroStepBorder int 34 | mx *sync.Mutex 35 | ctx context.Context 36 | cancel func() 37 | done map[int]struct{} 38 | 39 | resForSure chan *T 40 | } 41 | 42 | func newFirstResult[T any](totalSteps int) *firstResult[T] { 43 | ctx, cancel := context.WithCancel(context.Background()) 44 | return &firstResult[T]{ 45 | step: math.MaxInt, 46 | totalSteps: totalSteps, 47 | mx: &sync.Mutex{}, 48 | ctx: ctx, 49 | cancel: cancel, 50 | done: make(map[int]struct{}, totalSteps), 51 | resForSure: make(chan *T), 52 | } 53 | } 54 | 55 | func (r *firstResult[T]) setVal(val *T, step int) { 56 | r.mx.Lock() 57 | defer r.mx.Unlock() 58 | 59 | if step == r.zeroStepBorder { 60 | r.resForSure <- val 61 | r.cancel() 62 | return 63 | } 64 | if step < r.step { 65 | r.val = val 66 | r.step = step 67 | } 68 | } 69 | 70 | func (r *firstResult[T]) stepDone(step int) { 71 | r.mx.Lock() 72 | defer r.mx.Unlock() 73 | 74 | r.done[step] = struct{}{} 75 | 76 | // need to move r.zeroStepBorder up 77 | if step == r.zeroStepBorder { 78 | ok := true 79 | for ok { 80 | r.zeroStepBorder++ 81 | if r.zeroStepBorder == r.step { 82 | r.resForSure <- r.val 83 | r.cancel() 84 | return 85 | } 86 | 87 | _, ok = r.done[r.zeroStepBorder] 88 | } 89 | } 90 | 91 | if r.zeroStepBorder >= r.totalSteps { 92 | r.resForSure <- nil 93 | r.cancel() 94 | } 95 | } 96 | 97 | func first[T any](limit, grtCnt int, fn func(i int) (*T, bool)) (f *T) { 98 | if limit == 0 { 99 | return 100 | } 101 | step := max(divUp(limit, grtCnt), 1) 102 | tickets := genTickets(grtCnt) 103 | 104 | res := newFirstResult[T](grtCnt) 105 | 106 | stepCnt := 0 107 | for i := 0; i >= 0 && i < limit; i += step { 108 | <-tickets 109 | go func(lf, rg, stepCnt int) { 110 | defer func() { 111 | tickets <- struct{}{} 112 | }() 113 | 114 | done := res.ctx.Done() 115 | for j := lf; j < rg; j++ { 116 | // FIXME: this code is ugly but saves about 30% of time on locks 117 | if j%2 != 0 { 118 | val, skipped := fn(j) 119 | if !skipped { 120 | res.setVal(val, stepCnt) 121 | return 122 | } 123 | } else { 124 | select { 125 | case <-done: 126 | return 127 | default: 128 | val, skipped := fn(j) 129 | if !skipped { 130 | res.setVal(val, stepCnt) 131 | return 132 | } 133 | } 134 | } 135 | } 136 | res.stepDone(stepCnt) 137 | }(i, i+step, stepCnt) 138 | stepCnt++ 139 | } 140 | 141 | return <-res.resForSure 142 | } 143 | -------------------------------------------------------------------------------- /pkg/pipe/prefixpipe.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import "github.com/koss-null/funcfrog/internal/internalpipe" 4 | 5 | type entrails[T any] interface { 6 | Entrails() *internalpipe.Pipe[T] 7 | } 8 | 9 | // Map applies function on a Piper of type SrcT and returns a Pipe of type DstT. 10 | func Map[SrcT any, DstT any]( 11 | p Piper[SrcT], 12 | fn func(x SrcT) DstT, 13 | ) Piper[DstT] { 14 | pp := any(p).(entrails[SrcT]).Entrails() 15 | return &Pipe[DstT]{internalpipe.Pipe[DstT]{ 16 | Fn: func(i int) (*DstT, bool) { 17 | if obj, skipped := pp.Fn(i); !skipped { 18 | dst := fn(*obj) 19 | return &dst, false 20 | } 21 | return nil, true 22 | }, 23 | Len: pp.Len, 24 | ValLim: pp.ValLim, 25 | GoroutinesCnt: pp.GoroutinesCnt, 26 | }} 27 | } 28 | 29 | // MapNL applies function on a PiperNoLen of type SrcT and returns a Pipe of type DstT. 30 | func MapNL[SrcT, DstT any]( 31 | p PiperNoLen[SrcT], 32 | fn func(x SrcT) DstT, 33 | ) PiperNoLen[DstT] { 34 | pp := any(p).(entrails[SrcT]).Entrails() 35 | return &PipeNL[DstT]{internalpipe.Pipe[DstT]{ 36 | Fn: func(i int) (*DstT, bool) { 37 | if obj, skipped := pp.Fn(i); !skipped { 38 | dst := fn(*obj) 39 | return &dst, false 40 | } 41 | return nil, true 42 | }, 43 | Len: pp.Len, 44 | ValLim: pp.ValLim, 45 | GoroutinesCnt: pp.GoroutinesCnt, 46 | }} 47 | } 48 | 49 | // MapFilter applies function on a Piper of type SrcT and returns a Pipe of type DstT. 50 | // fn returns a value of DstT type and true if this value is not skipped. 51 | func MapFilter[SrcT, DstT any]( 52 | p Piper[SrcT], 53 | fn func(x SrcT) (DstT, bool), 54 | ) Piper[DstT] { 55 | pp := any(p).(entrails[SrcT]).Entrails() 56 | return &Pipe[DstT]{internalpipe.Pipe[DstT]{ 57 | Fn: func(i int) (*DstT, bool) { 58 | if obj, skipped := pp.Fn(i); !skipped { 59 | dst, exist := fn(*obj) 60 | return &dst, !exist 61 | } 62 | return nil, true 63 | }, 64 | Len: pp.Len, 65 | ValLim: pp.ValLim, 66 | GoroutinesCnt: pp.GoroutinesCnt, 67 | }} 68 | } 69 | 70 | // MapFilterNL applies function on a PiperNoLen of type SrcT and returns a Pipe of type DstT. 71 | // fn returns a value of DstT type and true if this value is not skipped. 72 | func MapFilterNL[SrcT, DstT any]( 73 | p PiperNoLen[SrcT], 74 | fn func(x SrcT) (DstT, bool), 75 | ) PiperNoLen[DstT] { 76 | pp := any(p).(entrails[SrcT]).Entrails() 77 | return &PipeNL[DstT]{internalpipe.Pipe[DstT]{ 78 | Fn: func(i int) (*DstT, bool) { 79 | if obj, skipped := pp.Fn(i); !skipped { 80 | dst, exist := fn(*obj) 81 | return &dst, !exist 82 | } 83 | return nil, true 84 | }, 85 | Len: pp.Len, 86 | ValLim: pp.ValLim, 87 | GoroutinesCnt: pp.GoroutinesCnt, 88 | }} 89 | } 90 | 91 | // Reduce applies reduce operation on Pipe of type SrcT and returns result of type DstT. 92 | // initVal is an optional parameter to initialize a value that should be used on the first step of reduce. 93 | func Reduce[SrcT, DstT any](p Piper[SrcT], fn func(*DstT, *SrcT) DstT, initVal ...DstT) DstT { 94 | var init DstT 95 | if len(initVal) > 0 { 96 | init = initVal[0] 97 | } 98 | data := p.Do() 99 | switch len(data) { 100 | case 0: 101 | return init 102 | case 1: 103 | return fn(&init, &data[0]) 104 | default: 105 | res := fn(&init, &data[0]) 106 | for i := range data[1:] { 107 | res = fn(&res, &data[i+1]) 108 | } 109 | return res 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /internal/algo/parallel/mergesort/mergesort.go: -------------------------------------------------------------------------------- 1 | package mergesort 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | "sync" 7 | 8 | "golang.org/x/exp/constraints" 9 | ) 10 | 11 | const ( 12 | singleThreadSortTreshold = 5000 13 | ) 14 | 15 | type border struct{ lf, rg int } 16 | 17 | // Sort is an inner implementation of a parallel merge sort where sort.Slice() 18 | // is used to sort sliced array parts 19 | func Sort[T any](data []T, less func(T, T) bool, threads int) []T { 20 | if len(data) < singleThreadSortTreshold { 21 | sort.Slice(data, func(i, j int) bool { 22 | return less(data[i], data[j]) 23 | }) 24 | return data 25 | } 26 | 27 | step := max(int(math.Ceil(float64(len(data))/float64(threads))), 1) 28 | splits := make([]border, 0, len(data)/step+1) 29 | lf, rg := 0, min(step, len(data)) 30 | var wg sync.WaitGroup 31 | for lf < len(data) { 32 | wg.Add(1) 33 | go func(lf, rg int) { 34 | d := data[lf:rg] 35 | cmp := func(i, j int) bool { 36 | return less(d[i], d[j]) 37 | } 38 | sort.Slice(d, cmp) 39 | wg.Done() 40 | }(lf, rg) 41 | splits = append(splits, border{lf: lf, rg: rg}) 42 | 43 | lf = rg 44 | rg = min(rg+step, len(data)) 45 | } 46 | wg.Wait() 47 | 48 | mergeSplits(data, splits, threads, less) 49 | return data 50 | } 51 | 52 | // mergeSplits is an inner functnon to merge all sorted splits of a slice into a one sorted slice 53 | func mergeSplits[T any]( 54 | data []T, 55 | splits []border, 56 | threads int, 57 | less func(T, T) bool, 58 | ) { 59 | jobTicket := make(chan struct{}, threads) 60 | for i := 0; i < threads; i++ { 61 | jobTicket <- struct{}{} 62 | } 63 | 64 | var wg sync.WaitGroup 65 | // FIXME: old splits array should be reused probably since it causes huge mem alloc now 66 | // FIXME: n^2 mem alloc here 67 | newSplits := make([]border, 0, len(splits)/2+1) 68 | for i := 0; i < len(splits); i += 2 { 69 | // this is the last iteration 70 | if i+1 >= len(splits) { 71 | newSplits = append(newSplits, splits[i]) 72 | break 73 | } 74 | 75 | // this one controls amount of simultaneous merge processes running 76 | <-jobTicket 77 | wg.Add(1) 78 | go func(i int) { 79 | merge( 80 | data, 81 | splits[i].lf, splits[i].rg, 82 | splits[i+1].lf, splits[i+1].rg, 83 | less, 84 | ) 85 | jobTicket <- struct{}{} 86 | wg.Done() 87 | }(i) 88 | newSplits = append( 89 | newSplits, 90 | border{ 91 | splits[i].lf, 92 | splits[i+1].rg, 93 | }, 94 | ) 95 | } 96 | wg.Wait() 97 | 98 | if len(newSplits) == 1 { 99 | return 100 | } 101 | mergeSplits(data, newSplits, threads, less) 102 | } 103 | 104 | // merge is an inner function to merge two sorted slices into one sorted slice 105 | func merge[T any](a []T, lf, rg, lf1, rg1 int, less func(T, T) bool) { 106 | st := lf 107 | res := make([]T, 0, rg1-lf) 108 | for lf < rg && lf1 < rg1 { 109 | if less(a[lf], a[lf1]) { 110 | res = append(res, a[lf]) 111 | lf++ 112 | continue 113 | } 114 | res = append(res, a[lf1]) 115 | lf1++ 116 | } 117 | // only one of the for[s] below is running 118 | for lf < rg { 119 | res = append(res, a[lf]) 120 | lf++ 121 | } 122 | for lf1 < rg1 { 123 | res = append(res, a[lf1]) 124 | lf1++ 125 | } 126 | copy(a[st:], res) 127 | } 128 | 129 | func min[T constraints.Ordered](a, b T) T { 130 | if a > b { 131 | return b 132 | } 133 | return a 134 | } 135 | 136 | func max[T constraints.Ordered](a, b T) T { 137 | if a < b { 138 | return b 139 | } 140 | return a 141 | } 142 | -------------------------------------------------------------------------------- /internal/internalpipe/mapfilterer_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_MapFilter(t *testing.T) { 10 | t.Parallel() 11 | 12 | exp := make([]int, 0, 100_000) 13 | for i := 0; i < 100_000; i++ { 14 | if i%2 == 0 { 15 | exp = append(exp, i+1) 16 | } 17 | } 18 | 19 | t.Run("single thread lim set", func(t *testing.T) { 20 | p := Pipe[int]{ 21 | Fn: func(i int) (*int, bool) { 22 | return &i, false 23 | }, 24 | Len: 100_000, 25 | ValLim: -1, 26 | GoroutinesCnt: 1, 27 | } 28 | res := p.MapFilter(func(x int) (int, bool) { return x + 1, x%2 == 0 }). 29 | Do() 30 | 31 | require.Equal(t, len(exp), len(res)) 32 | for i, r := range res { 33 | require.Equal(t, exp[i], r) 34 | } 35 | }) 36 | 37 | t.Run("seven thread lim set", func(t *testing.T) { 38 | p := Pipe[int]{ 39 | Fn: func(i int) (*int, bool) { 40 | return &i, false 41 | }, 42 | Len: 100_000, 43 | ValLim: -1, 44 | GoroutinesCnt: 7, 45 | } 46 | 47 | res := p.MapFilter(func(x int) (int, bool) { return x + 1, x%2 == 0 }). 48 | Parallel(7).Do() 49 | 50 | for i, r := range res { 51 | require.Equal(t, exp[i], r) 52 | } 53 | }) 54 | 55 | t.Run("single thread ValLim set", func(t *testing.T) { 56 | p := Pipe[int]{ 57 | Fn: func(i int) (*int, bool) { 58 | return &i, false 59 | }, 60 | Len: -1, 61 | ValLim: len(exp), 62 | GoroutinesCnt: 1, 63 | } 64 | res := p.MapFilter(func(x int) (int, bool) { return x + 1, x%2 == 0 }). 65 | Do() 66 | 67 | require.Equal(t, len(exp), len(res)) 68 | for i, r := range res { 69 | require.Equal(t, exp[i], r) 70 | } 71 | }) 72 | 73 | t.Run("seven thread ValLim set", func(t *testing.T) { 74 | p := Pipe[int]{ 75 | Fn: func(i int) (*int, bool) { 76 | return &i, false 77 | }, 78 | Len: -1, 79 | ValLim: len(exp), 80 | GoroutinesCnt: 7, 81 | } 82 | res := p.MapFilter(func(x int) (int, bool) { return x + 1, x%2 == 0 }). 83 | Do() 84 | 85 | require.Equal(t, len(exp), len(res)) 86 | for i, r := range res { 87 | require.Equal(t, exp[i], r) 88 | } 89 | }) 90 | 91 | t.Run("seven thread ValLim multiple calls", func(t *testing.T) { 92 | p := Pipe[int]{ 93 | Fn: func(i int) (*int, bool) { 94 | return &i, false 95 | }, 96 | Len: -1, 97 | ValLim: len(exp), 98 | GoroutinesCnt: 7, 99 | } 100 | res := p.MapFilter(func(x int) (int, bool) { return x + 1, x%2 == 0 }). 101 | MapFilter(func(x int) (int, bool) { return x, true }). 102 | Do() 103 | 104 | require.Equal(t, len(exp), len(res)) 105 | for i, r := range res { 106 | require.Equal(t, exp[i], r) 107 | } 108 | }) 109 | 110 | t.Run("single thread lim set empty", func(t *testing.T) { 111 | p := Pipe[int]{ 112 | Fn: func(i int) (*int, bool) { 113 | return &i, false 114 | }, 115 | Len: 100_000, 116 | ValLim: -1, 117 | GoroutinesCnt: 1, 118 | } 119 | res := p.MapFilter(func(x int) (int, bool) { return x + 1, false }).Do() 120 | 121 | require.Equal(t, 0, len(res)) 122 | }) 123 | 124 | t.Run("seven thread lim set empty", func(t *testing.T) { 125 | p := Pipe[int]{ 126 | Fn: func(i int) (*int, bool) { 127 | return &i, false 128 | }, 129 | Len: 100_000, 130 | ValLim: -1, 131 | GoroutinesCnt: 7, 132 | } 133 | 134 | res := p.MapFilter(func(x int) (int, bool) { return x + 1, false }). 135 | Parallel(7).Do() 136 | 137 | require.Equal(t, 0, len(res)) 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/pipe/pipe.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "github.com/koss-null/funcfrog/internal/internalpipe" 5 | ) 6 | 7 | // Pipe implements the pipe on any slice. 8 | // Pipe may be initialized with `Slice`, `Func`, `Cycle` or `Range`. 9 | type Pipe[T any] struct { 10 | internalpipe.Pipe[T] 11 | } 12 | 13 | // Map applies given function to each element of the underlying slice 14 | // returns the slice where each element is n[i] = f(p[i]). 15 | func (p *Pipe[T]) Map(fn func(T) T) Piper[T] { 16 | return &Pipe[T]{p.Pipe.Map(fn)} 17 | } 18 | 19 | // Filter leaves only items with true predicate fn. 20 | func (p *Pipe[T]) Filter(fn Predicate[T]) Piper[T] { 21 | return &Pipe[T]{p.Pipe.Filter(fn)} 22 | } 23 | 24 | // MapFilter applies given function to each element of the underlying slice, 25 | // if the second returning value of fn is false, the element is skipped (may be useful for error handling). 26 | // returns the slice where each element is n[i] = f(p[i]) if it is not skipped. 27 | func (p *Pipe[T]) MapFilter(fn func(T) (T, bool)) Piper[T] { 28 | return &Pipe[T]{p.Pipe.MapFilter(fn)} 29 | } 30 | 31 | // Sort sorts the underlying slice on a current step of a pipeline. 32 | func (p *Pipe[T]) Sort(less Comparator[T]) Piper[T] { 33 | return &Pipe[T]{p.Pipe.Sort(less)} 34 | } 35 | 36 | // Reduce applies the result of a function to each element one-by-one: f(p[n], f(p[n-1], f(p[n-2, ...]))). 37 | // It is recommended to use reducers from the default reducer if possible to decrease memory allocations. 38 | func (p *Pipe[T]) Reduce(fn Accum[T]) *T { 39 | return p.Pipe.Reduce(internalpipe.AccumFn[T](fn)) 40 | } 41 | 42 | // Sum returns the sum of all elements. It is similar to Reduce but is able to work in parallel. 43 | func (p *Pipe[T]) Sum(plus Accum[T]) T { 44 | return p.Pipe.Sum(internalpipe.AccumFn[T](plus)) 45 | } 46 | 47 | // First returns the first element of the pipe. 48 | func (p *Pipe[T]) First() *T { 49 | return p.Pipe.First() 50 | } 51 | 52 | // Any returns a pointer to a random element in the pipe or nil if none left. 53 | func (p *Pipe[T]) Any() *T { 54 | return p.Pipe.Any() 55 | } 56 | 57 | // Parallel set n - the amount of goroutines to run on. 58 | // Only the first Parallel() in a pipe chain is applied. 59 | func (p *Pipe[T]) Parallel(n uint16) Piper[T] { 60 | return &Pipe[T]{p.Pipe.Parallel(n)} 61 | } 62 | 63 | // Do evaluates all the pipeline and returns the result slice. 64 | func (p *Pipe[T]) Do() []T { 65 | return p.Pipe.Do() 66 | } 67 | 68 | // Count evaluates all the pipeline and returns the amount of items. 69 | func (p *Pipe[T]) Count() int { 70 | return p.Pipe.Count() 71 | } 72 | 73 | // Promices returns an array of Promice values - functions to be evaluated to get the value on i'th place. 74 | // Promice returns two values: the evaluated value and if it is not skipped. 75 | func (p *Pipe[T]) Promices() []func() (T, bool) { 76 | return p.Pipe.Promices() 77 | } 78 | 79 | // Erase wraps all pipe values to interface{} type, so you are able to use pipe methods with type convertions. 80 | // You can use collectors from collectiors.go file of this package to collect results into a particular type. 81 | func (p *Pipe[T]) Erase() Piper[any] { 82 | return &Pipe[any]{p.Pipe.Erase()} 83 | } 84 | 85 | // Snag links an error handler to the previous Pipe method. 86 | func (p *Pipe[T]) Snag(h func(error)) Piper[T] { 87 | return &Pipe[T]{p.Pipe.Snag(internalpipe.ErrHandler(h))} 88 | } 89 | 90 | // Yeti links a yeti error handler to the Pipe. 91 | func (p *Pipe[T]) Yeti(y internalpipe.YeetSnag) Piper[T] { 92 | return &Pipe[T]{p.Pipe.Yeti(y)} 93 | } 94 | 95 | // Entrails is an out-of-Piper interface method to provide Map[T1 -> T2]. 96 | func (p *Pipe[T]) Entrails() *internalpipe.Pipe[T] { 97 | return &p.Pipe 98 | } 99 | -------------------------------------------------------------------------------- /internal/internalpipe/constructor_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Slice(t *testing.T) { 11 | t.Parallel() 12 | 13 | a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 14 | p := Slice(a) 15 | require.Equal(t, p.Len, 10) 16 | require.Equal(t, p.ValLim, notSet) 17 | require.Equal(t, p.GoroutinesCnt, defaultParallelWrks) 18 | r, _ := p.Fn(5) 19 | require.Equal(t, *r, a[5]) 20 | _, skipped := p.Fn(15) 21 | require.True(t, skipped) 22 | } 23 | 24 | func Test_FuncP(t *testing.T) { 25 | t.Parallel() 26 | 27 | p := FuncP(func(i int) (*int, bool) { 28 | return &i, true 29 | }) 30 | require.Equal(t, p.Len, notSet) 31 | require.Equal(t, p.ValLim, notSet) 32 | require.Equal(t, p.GoroutinesCnt, defaultParallelWrks) 33 | 34 | res := p.Gen(5).Do() 35 | require.Equal(t, []int{0, 1, 2, 3, 4}, res) 36 | } 37 | 38 | func Not[T any](fn func(x T) bool) func(T) bool { 39 | return func(x T) bool { 40 | return !fn(x) 41 | } 42 | } 43 | 44 | func Test_Cycle(t *testing.T) { 45 | t.Parallel() 46 | 47 | isVowel := func(s *string) bool { 48 | st := struct{}{} 49 | _, ok := map[string]struct{}{"a": st, "e": st, "i": st, "o": st, "u": st, "y": st}[*s] 50 | return ok 51 | } 52 | 53 | t.Run("happy", func(t *testing.T) { 54 | t.Parallel() 55 | p := Cycle([]string{"a", "b", "c", "d"}) 56 | require.Equal( 57 | t, 58 | []string{"A", "A", "A", "A", "A", "A", "A", "A", "A", "A"}, 59 | p.Filter(isVowel).Map(strings.ToUpper).Take(10).Do(), 60 | ) 61 | }) 62 | 63 | t.Run("empty res", func(t *testing.T) { 64 | t.Parallel() 65 | p := Cycle([]string{"a", "b", "c", "d"}) 66 | require.Equal( 67 | t, 68 | []string{}, 69 | p.Filter(isVowel).Filter(Not(isVowel)).Gen(10).Do(), 70 | ) 71 | }) 72 | 73 | t.Run("empty cycle", func(t *testing.T) { 74 | t.Parallel() 75 | p := Cycle([]string{}) 76 | require.Equal( 77 | t, 78 | []string{}, 79 | p.Filter(isVowel). 80 | Map(strings.TrimSpace). 81 | Gen(10). 82 | Do(), 83 | ) 84 | }) 85 | } 86 | 87 | func Test_Range(t *testing.T) { 88 | t.Parallel() 89 | 90 | t.Run("happy", func(t *testing.T) { 91 | t.Parallel() 92 | p := Range(0, 10, 1) 93 | res := p.Do() 94 | require.Equal(t, 10, len(res)) 95 | for i := 0; i < 10; i++ { 96 | require.Equal(t, i, res[i]) 97 | } 98 | }) 99 | t.Run("single_step_owerflow", func(t *testing.T) { 100 | t.Parallel() 101 | p := Range(1, 10, 50) 102 | res := p.Do() 103 | require.Equal(t, 1, len(res)) 104 | require.Equal(t, 1, res[0]) 105 | }) 106 | t.Run("finish_is_less_than_start", func(t *testing.T) { 107 | t.Parallel() 108 | p := Range(100, 10, 50) 109 | res := p.Do() 110 | require.Equal(t, 0, len(res)) 111 | }) 112 | t.Run("step_is_0", func(t *testing.T) { 113 | t.Parallel() 114 | p := Range(1, 10, 0) 115 | res := p.Do() 116 | require.Equal(t, 0, len(res)) 117 | }) 118 | t.Run("start_is_finish", func(t *testing.T) { 119 | t.Parallel() 120 | p := Range(1, 1, 1) 121 | res := p.Do() 122 | require.Equal(t, 0, len(res)) 123 | }) 124 | t.Run("start_is_finish_negative", func(t *testing.T) { 125 | t.Parallel() 126 | p := Range(1, 1, -1) 127 | res := p.Do() 128 | require.Equal(t, 0, len(res)) 129 | }) 130 | t.Run("finish_is_less_than_start_and_step_is_negative", func(t *testing.T) { 131 | t.Parallel() 132 | p := Range(100, 10, -50) 133 | res := p.Do() 134 | require.Equal(t, 2, len(res)) 135 | require.Equal(t, 100, res[0]) 136 | require.Equal(t, 50, res[1]) 137 | }) 138 | } 139 | 140 | func Test_Repeat(t *testing.T) { 141 | t.Parallel() 142 | 143 | t.Run("happy", func(t *testing.T) { 144 | t.Parallel() 145 | 146 | p := Repeat("hello", 5).Map(strings.ToUpper).Do() 147 | require.Equal(t, []string{"HELLO", "HELLO", "HELLO", "HELLO", "HELLO"}, p) 148 | }) 149 | 150 | t.Run("n==0", func(t *testing.T) { 151 | t.Parallel() 152 | 153 | p := Repeat("hello", 0).Map(strings.ToUpper).Do() 154 | require.Equal(t, []string{}, p) 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /internal/algo/parallel/qsort/qsort_test.go: -------------------------------------------------------------------------------- 1 | package qsort 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // saving random array to make tests faster 13 | var ( 14 | rndAr []int 15 | rndMx sync.Mutex 16 | ) 17 | 18 | func rnd(n int) []int { 19 | rndMx.Lock() 20 | defer rndMx.Unlock() 21 | 22 | if len(rndAr) >= n { 23 | return rndAr[:n] 24 | } 25 | 26 | res := make([]int, n) 27 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 28 | for i := 0; i < n; i++ { 29 | res[i] = r.Intn(n) 30 | } 31 | rndAr = res 32 | return res 33 | } 34 | 35 | func Test_partition(t *testing.T) { 36 | a := rnd(1000) 37 | q := partition(a, 0, len(a)-1, func(a, b *int) bool { return *a < *b }) 38 | for i := 0; i <= q; i++ { 39 | for j := q + 1; j < len(a); j++ { 40 | require.LessOrEqual(t, a[i], a[j]) 41 | } 42 | } 43 | } 44 | 45 | func Test_Sort(t *testing.T) { 46 | a := rnd(6000) 47 | Sort(a, func(a, b *int) bool { return *a < *b }, 3) 48 | for i := range a { 49 | if i != 0 { 50 | require.GreaterOrEqual(t, a[i], a[i-1]) 51 | } 52 | } 53 | } 54 | 55 | func Test_Sort_1thread(t *testing.T) { 56 | a := rnd(6000) 57 | Sort(a, func(a, b *int) bool { return *a < *b }, 1) 58 | for i := range a { 59 | if i != 0 { 60 | require.GreaterOrEqual(t, a[i], a[i-1]) 61 | } 62 | } 63 | } 64 | 65 | func Test_Sort_0thread(t *testing.T) { 66 | a := rnd(6000) 67 | Sort(a, func(a, b *int) bool { return *a < *b }, 0) 68 | for i := range a { 69 | if i != 0 { 70 | require.GreaterOrEqual(t, a[i], a[i-1]) 71 | } 72 | } 73 | } 74 | 75 | func Test_Sort_One(t *testing.T) { 76 | a := rnd(1) 77 | zeroIdxVal := a[0] 78 | Sort(a, func(a, b *int) bool { return *a < *b }, 3) 79 | for i := range a { 80 | if i != 0 { 81 | require.GreaterOrEqual(t, a[i], a[i-1]) 82 | } 83 | } 84 | require.Equal(t, len(a), 1) 85 | require.Equal(t, a[0], zeroIdxVal) 86 | } 87 | 88 | func Test_qsort(t *testing.T) { 89 | a := rnd(6000) 90 | tickets := genTickets(3) 91 | var wg sync.WaitGroup 92 | wg.Add(1) 93 | qsort(a, 0, len(a)-1, func(a, b *int) bool { return *a < *b }, tickets, &wg) 94 | wg.Wait() 95 | for i := range a { 96 | if i != 0 { 97 | require.GreaterOrEqual(t, a[i], a[i-1]) 98 | } 99 | } 100 | } 101 | 102 | func Test_sort_one(t *testing.T) { 103 | a := rnd(1) 104 | require.Equal(t, len(a), 1) 105 | tickets := genTickets(3) 106 | var wg sync.WaitGroup 107 | wg.Add(1) 108 | qsort(a, 0, len(a)-1, func(a, b *int) bool { return *a < *b }, tickets, &wg) 109 | wg.Wait() 110 | require.Equal(t, len(a), 1) 111 | } 112 | 113 | func Test_sort_big(t *testing.T) { 114 | a := rnd(100_000) 115 | tickets := genTickets(6) 116 | var wg sync.WaitGroup 117 | wg.Add(1) 118 | qsort(a, 0, len(a)-1, func(a, b *int) bool { return *a < *b }, tickets, &wg) 119 | wg.Wait() 120 | for i := range a { 121 | if i != 0 { 122 | require.GreaterOrEqual(t, a[i], a[i-1]) 123 | } 124 | } 125 | } 126 | 127 | func Test_sort_huge(t *testing.T) { 128 | a := rnd(500_000) 129 | tickets := genTickets(12) 130 | var wg sync.WaitGroup 131 | wg.Add(1) 132 | qsort(a, 0, len(a)-1, func(a, b *int) bool { return *a < *b }, tickets, &wg) 133 | wg.Wait() 134 | for i := range a { 135 | if i != 0 { 136 | require.GreaterOrEqual(t, a[i], a[i-1]) 137 | } 138 | } 139 | } 140 | 141 | func Test_sort_same(t *testing.T) { 142 | a := make([]int, 10000) 143 | for i := 0; i < 5000; i++ { 144 | a[i] = (i + 10) * (i - 1) 145 | } // others 5000 are 0 146 | tickets := genTickets(6) 147 | var wg sync.WaitGroup 148 | wg.Add(1) 149 | qsort(a, 0, len(a)-1, func(a, b *int) bool { return *a < *b }, tickets, &wg) 150 | wg.Wait() 151 | for i := range a { 152 | if i != 0 { 153 | require.GreaterOrEqual(t, a[i], a[i-1]) 154 | } 155 | } 156 | } 157 | 158 | func Test_median(t *testing.T) { 159 | val, idx := median([...]int{1, 1, 3}, func(x, y *int) bool { return *x < *y }) 160 | require.Equal(t, 1, *val) 161 | require.Equal(t, int16(0), idx) 162 | 163 | val, idx = median([...]int{0, 1, 1}, func(x, y *int) bool { return *x < *y }) 164 | require.Equal(t, 1, *val) 165 | require.Equal(t, int16(1), idx) 166 | 167 | val, idx = median([...]int{1, 2, 1}, func(x, y *int) bool { return *x < *y }) 168 | require.Equal(t, 1, *val) 169 | require.Equal(t, int16(2), idx) 170 | } 171 | -------------------------------------------------------------------------------- /internal/internalpipe/any_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var ( 11 | once sync.Once 12 | a100k []float64 13 | ) 14 | 15 | func initA100k() { 16 | once.Do(func() { 17 | a100k = make([]float64, 100_000) 18 | for i := range a100k { 19 | a100k[i] = float64(i) 20 | } 21 | }) 22 | } 23 | 24 | func TestAny(t *testing.T) { 25 | initA100k() 26 | t.Parallel() 27 | 28 | t.Run("Single thread no limit", func(t *testing.T) { 29 | t.Parallel() 30 | 31 | p := Func(func(i int) (float64, bool) { 32 | return a100k[i], a100k[i] > 90_000.0 33 | }) 34 | s := p.Any() 35 | require.NotNil(t, s) 36 | require.Greater(t, *s, 90_000.0) 37 | }) 38 | 39 | t.Run("Single thread limit", func(t *testing.T) { 40 | t.Parallel() 41 | 42 | p := Func(func(i int) (float64, bool) { 43 | return a100k[i], a100k[i] > 90_000.0 44 | }).Gen(len(a100k)) 45 | s := p.Any() 46 | require.NotNil(t, s) 47 | require.Greater(t, *s, 90_000.0) 48 | }) 49 | 50 | t.Run("Seven thread no limit", func(t *testing.T) { 51 | t.Parallel() 52 | 53 | p := Func(func(i int) (float64, bool) { 54 | if i >= len(a100k) { 55 | return 0., false 56 | } 57 | return a100k[i], true 58 | }). 59 | Filter(func(x *float64) bool { return *x > 90_000. }). 60 | Parallel(7) 61 | s := p.Any() 62 | require.NotNil(t, s) 63 | require.Greater(t, *s, 90_000.0) 64 | }) 65 | 66 | t.Run("Seven thread limit", func(t *testing.T) { 67 | t.Parallel() 68 | 69 | p := Func(func(i int) (float64, bool) { 70 | if i >= len(a100k) { 71 | return 0., false 72 | } 73 | return a100k[i], a100k[i] > 90_000.0 74 | }).Gen(len(a100k)).Parallel(7) 75 | s := p.Any() 76 | require.NotNil(t, s) 77 | require.Greater(t, *s, 90_000.0) 78 | }) 79 | 80 | t.Run("Single thread NF limit", func(t *testing.T) { 81 | t.Parallel() 82 | 83 | p := Func(func(i int) (float64, bool) { 84 | return a100k[i], false 85 | }).Gen(len(a100k)) 86 | s := p.Any() 87 | require.Nil(t, s) 88 | }) 89 | 90 | t.Run("Seven thread NF limit", func(t *testing.T) { 91 | t.Parallel() 92 | 93 | p := Func(func(i int) (float64, bool) { 94 | if i >= len(a100k) { 95 | return 0., false 96 | } 97 | return a100k[i], false 98 | }).Gen(len(a100k)).Parallel(7) 99 | s := p.Any() 100 | require.Nil(t, s) 101 | }) 102 | 103 | t.Run("Single thread bounded limit", func(t *testing.T) { 104 | t.Parallel() 105 | 106 | p := Func(func(i int) (float64, bool) { 107 | return a100k[i], false 108 | }).Gen(len(a100k)) 109 | s := p.Any() 110 | require.Nil(t, s) 111 | }) 112 | 113 | t.Run("Seven thread bounded limit", func(t *testing.T) { 114 | t.Parallel() 115 | 116 | p := Func(func(i int) (float64, bool) { 117 | if i >= len(a100k) { 118 | return 0., false 119 | } 120 | return a100k[i], a100k[i] > 90_000.0 && a100k[i] < 90_002.0 121 | }).Gen(len(a100k)).Parallel(7) 122 | s := p.Any() 123 | require.NotNil(t, s) 124 | require.Equal(t, 90_001., *s) 125 | }) 126 | 127 | t.Run("Single thread bounded no limit", func(t *testing.T) { 128 | t.Parallel() 129 | 130 | p := Func(func(i int) (float64, bool) { 131 | if i >= len(a100k) { 132 | return 0., false 133 | } 134 | return a100k[i], a100k[i] > 90_000.0 && a100k[i] < 90_002.0 135 | }) 136 | s := p.Any() 137 | require.NotNil(t, s) 138 | require.Equal(t, 90_001., *s) 139 | }) 140 | 141 | t.Run("Seven thread bounded no limit", func(t *testing.T) { 142 | t.Parallel() 143 | 144 | p := Func(func(i int) (float64, bool) { 145 | if i >= len(a100k) { 146 | return 0., false 147 | } 148 | return a100k[i], a100k[i] > 90_000.0 && a100k[i] < 90_002.0 149 | }).Parallel(7) 150 | s := p.Any() 151 | require.NotNil(t, s) 152 | require.Equal(t, 90_001., *s) 153 | }) 154 | 155 | t.Run("Ten thread bounded no limit filter", func(t *testing.T) { 156 | t.Parallel() 157 | 158 | p := Func(func(i int) (float64, bool) { 159 | if i >= len(a100k) { 160 | return 0., false 161 | } 162 | return a100k[i], a100k[i] > 90_000.0 && a100k[i] < 190_000.0 163 | }).Filter(func(x *float64) bool { return int(*x)%2 == 0 }).Parallel(10) 164 | s := p.Any() 165 | require.NotNil(t, s) 166 | require.Greater(t, *s, 90_000.) 167 | require.Less(t, *s, 190_001.) 168 | }) 169 | } 170 | -------------------------------------------------------------------------------- /pkg/pipies/pipies_test.go: -------------------------------------------------------------------------------- 1 | package pipies 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/koss-null/funcfrog/internal/primitive/pointer" 9 | "github.com/koss-null/funcfrog/pkg/pipe" 10 | ) 11 | 12 | type testT string 13 | 14 | func Test_Predicates(t *testing.T) { 15 | t.Parallel() 16 | 17 | t.Run("NotNil", func(t *testing.T) { 18 | t.Parallel() 19 | require.True(t, NotNil(pointer.Ref(1))) 20 | require.False(t, NotNil[int](nil)) 21 | require.False(t, NotNil[int](nil)) 22 | require.False(t, NotNil[testT](nil)) 23 | var empty *testT 24 | require.False(t, NotNil(empty)) 25 | empty = nil 26 | require.False(t, NotNil(empty)) 27 | var e any = empty 28 | require.True(t, NotNil(&e)) 29 | }) 30 | t.Run("IsNil", func(t *testing.T) { 31 | t.Parallel() 32 | require.False(t, IsNil(pointer.Ref(1))) 33 | require.True(t, IsNil[int](nil)) 34 | require.True(t, IsNil[int](nil)) 35 | require.True(t, IsNil[testT](nil)) 36 | var empty *testT 37 | require.True(t, IsNil(empty)) 38 | empty = nil 39 | require.True(t, IsNil(empty)) 40 | var e any = empty 41 | require.False(t, IsNil[any](&e)) 42 | }) 43 | t.Run("NotZero", func(t *testing.T) { 44 | t.Parallel() 45 | require.True(t, NotZero(pointer.Ref(1))) 46 | require.False(t, NotZero(pointer.Ref(0))) 47 | require.False(t, NotZero(pointer.Ref[float32](0.0))) 48 | require.False(t, NotZero(&struct{ a, b, c int }{})) 49 | require.True(t, NotZero(&struct{ a, b, c int }{1, 0, 0})) 50 | }) 51 | } 52 | 53 | func Test_PredicateBuilders(t *testing.T) { 54 | t.Parallel() 55 | 56 | t.Run("Eq", func(t *testing.T) { 57 | t.Parallel() 58 | eq5 := Eq(5) 59 | require.True(t, eq5(pointer.Ref(5))) 60 | require.False(t, eq5(pointer.Ref(4))) 61 | eqS := Eq("test") 62 | require.True(t, eqS(pointer.Ref("test"))) 63 | require.False(t, eqS(pointer.Ref("sett"))) 64 | }) 65 | 66 | t.Run("NotEq", func(t *testing.T) { 67 | t.Parallel() 68 | neq5 := NotEq(5) 69 | require.False(t, neq5(pointer.Ref(5))) 70 | require.True(t, neq5(pointer.Ref(4))) 71 | neqS := NotEq("test") 72 | require.False(t, neqS(pointer.Ref("test"))) 73 | require.True(t, neqS(pointer.Ref("sett"))) 74 | }) 75 | 76 | t.Run("LessThan", func(t *testing.T) { 77 | t.Parallel() 78 | lt5 := LessThan(5) 79 | require.True(t, lt5(pointer.Ref(4))) 80 | require.False(t, lt5(pointer.Ref(5))) 81 | require.False(t, lt5(pointer.Ref(6))) 82 | ltf5 := LessThan(5.0) 83 | require.True(t, ltf5(pointer.Ref(4.999))) 84 | require.False(t, ltf5(pointer.Ref(float64(int(5))))) 85 | require.False(t, ltf5(pointer.Ref(5.01))) 86 | }) 87 | } 88 | 89 | func Test_Comparator(t *testing.T) { 90 | t.Parallel() 91 | require.True(t, Less(pointer.Ref(4), pointer.Ref(5))) 92 | require.False(t, Less(pointer.Ref(5), pointer.Ref(5))) 93 | require.False(t, Less(pointer.Ref(6), pointer.Ref(5))) 94 | require.True(t, Less(pointer.Ref(4.999), pointer.Ref(5.0))) 95 | require.False(t, Less(pointer.Ref(float64(int(5))), pointer.Ref(5.0))) 96 | require.False(t, Less(pointer.Ref(5.01), pointer.Ref(5.0))) 97 | } 98 | 99 | func Test_Accum(t *testing.T) { 100 | t.Parallel() 101 | require.Equal(t, Sum(pointer.Ref(10), pointer.Ref(20)), 30) 102 | require.Equal(t, Sum(pointer.Ref(10.0), pointer.Ref(20.0)), 30.0) 103 | } 104 | 105 | func Test_Not(t *testing.T) { 106 | t.Parallel() 107 | require.Equal(t, Not(func(a bool) bool { return a })(true), false) 108 | require.Equal(t, Nott(func(a, b bool) bool { return a && b })(true, true), false) 109 | require.Equal(t, Nottt(func(a, b, c bool) bool { return a && b && c })(true, true, true), false) 110 | 111 | require.Equal(t, Not(func(a bool) bool { return a })(false), true) 112 | require.Equal(t, Nott(func(a, b bool) bool { return a && b })(false, false), true) 113 | require.Equal(t, Nottt(func(a, b, c bool) bool { return a && b && c })(false, false, false), true) 114 | } 115 | 116 | func Test_Distinct(t *testing.T) { 117 | t.Parallel() 118 | 119 | z, o, w, r, fv := 0, 1, 2, 3, 5 120 | getKey := func(x **int) int { return **x } 121 | predicate := Distinct(getKey) 122 | filtered := pipe.Slice([]*int{&o, &w, &r, &r, &w, &w, &o, &fv, &w, &r, &z}).Filter(predicate).Do() 123 | 124 | found := make(map[int]struct{}) 125 | for _, f := range filtered { 126 | _, ok := found[*f] 127 | require.False(t, ok, "Distinct element is duplicated") 128 | found[*f] = struct{}{} 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /internal/internalpipe/pipe_test.go: -------------------------------------------------------------------------------- 1 | package internalpipe 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_min(t *testing.T) { 11 | t.Parallel() 12 | require.Equal(t, min(1, 2), 1) 13 | require.Equal(t, min(2, 1), 1) 14 | require.Equal(t, min(1.1, 1.11), 1.1) 15 | require.Equal(t, min(0, -1), -1) 16 | require.Equal(t, min(0.0, -0.1), -0.1) 17 | require.Equal(t, min(0.0, 0.0), 0.0) 18 | } 19 | 20 | func Test_max(t *testing.T) { 21 | t.Parallel() 22 | require.Equal(t, max(1, 2), 2) 23 | require.Equal(t, max(1.1, 1.11), 1.11) 24 | require.Equal(t, max(0, -1), 0) 25 | require.Equal(t, max(0.0, -0.1), 0.0) 26 | require.Equal(t, max(0.0, 0.0), 0.0) 27 | } 28 | 29 | func Test_divUp(t *testing.T) { 30 | t.Parallel() 31 | require.Equal(t, divUp(121, 1), 121) 32 | require.Equal(t, divUp(121, 2), 61) 33 | require.Equal(t, divUp(5, 5), 1) 34 | require.Equal(t, divUp(5, 4), 2) 35 | require.Equal(t, divUp(5, 100000000), 1) 36 | require.Equal(t, divUp(1, 1345), 1) 37 | } 38 | 39 | func Test_genTickets(t *testing.T) { 40 | t.Parallel() 41 | require.Equal(t, len(genTickets(100)), 100) 42 | } 43 | 44 | func Test_do(t *testing.T) { 45 | t.Parallel() 46 | 47 | p := Pipe[int]{ 48 | Fn: func(i int) (*int, bool) { 49 | return &i, false 50 | }, 51 | Len: 10000, 52 | ValLim: -1, 53 | GoroutinesCnt: 5, 54 | } 55 | res := p.Do() 56 | for i := 0; i < 10000; i++ { 57 | require.Equal(t, i, res[i]) 58 | } 59 | } 60 | 61 | func Test_Parallel(t *testing.T) { 62 | t.Parallel() 63 | 64 | p := Pipe[int]{ 65 | Fn: func(i int) (*int, bool) { 66 | return &i, false 67 | }, 68 | Len: 10000, 69 | ValLim: -1, 70 | GoroutinesCnt: defaultParallelWrks, 71 | } 72 | p = p.Parallel(10) 73 | require.Equal(t, 10, p.GoroutinesCnt) 74 | p = p.Parallel(5) // second one should not be applied 75 | require.Equal(t, 10, p.GoroutinesCnt) 76 | 77 | p = Pipe[int]{ 78 | Fn: func(i int) (*int, bool) { 79 | return &i, false 80 | }, 81 | Len: 10000, 82 | ValLim: -1, 83 | GoroutinesCnt: defaultParallelWrks, 84 | } 85 | p = p.Parallel(0) 86 | require.Equal(t, defaultParallelWrks, p.GoroutinesCnt) 87 | p = p.Parallel(4) 88 | require.Equal(t, 4, p.GoroutinesCnt) 89 | } 90 | 91 | func Test_Take(t *testing.T) { 92 | t.Parallel() 93 | 94 | p := Pipe[int]{ 95 | Fn: func(i int) (*int, bool) { 96 | return &i, false 97 | }, 98 | Len: -1, 99 | ValLim: -1, 100 | GoroutinesCnt: 5, 101 | } 102 | p = p.Take(10) 103 | require.Equal(t, 10, p.limit()) 104 | p = p.Take(5) 105 | require.Equal(t, 10, p.limit()) 106 | p = p.Gen(5) 107 | require.Equal(t, 10, p.limit()) 108 | 109 | p = Pipe[int]{ 110 | Fn: func(i int) (*int, bool) { 111 | return &i, false 112 | }, 113 | Len: -1, 114 | ValLim: -1, 115 | GoroutinesCnt: 5, 116 | } 117 | p = p.Take(-1) 118 | require.Equal(t, math.MaxInt-1, p.limit()) 119 | p = p.Take(0) 120 | require.Equal(t, 0, p.limit()) 121 | p = p.Take(4) 122 | require.Equal(t, 0, p.limit()) 123 | } 124 | 125 | func Test_Gen(t *testing.T) { 126 | t.Parallel() 127 | 128 | p := Pipe[int]{ 129 | Fn: func(i int) (*int, bool) { 130 | return &i, false 131 | }, 132 | Len: -1, 133 | ValLim: -1, 134 | GoroutinesCnt: 5, 135 | } 136 | p = p.Gen(10) 137 | require.Equal(t, 10, p.limit()) 138 | p = p.Gen(5) 139 | require.Equal(t, 10, p.limit()) 140 | p = p.Take(5) 141 | require.Equal(t, 10, p.limit()) 142 | 143 | p = Pipe[int]{ 144 | Fn: func(i int) (*int, bool) { 145 | return &i, false 146 | }, 147 | Len: -1, 148 | ValLim: -1, 149 | GoroutinesCnt: 5, 150 | } 151 | p = p.Gen(-1) 152 | require.Equal(t, math.MaxInt-1, p.limit()) 153 | p = p.Gen(0) 154 | require.Equal(t, 0, p.limit()) 155 | p = p.Gen(4) 156 | require.Equal(t, 0, p.limit()) 157 | } 158 | 159 | func Test_Count(t *testing.T) { 160 | t.Parallel() 161 | 162 | p := Pipe[int]{ 163 | Fn: func(i int) (*int, bool) { 164 | return &i, false 165 | }, 166 | Len: 10000, 167 | ValLim: -1, 168 | GoroutinesCnt: 5, 169 | } 170 | require.Equal(t, 10000, p.Count()) 171 | 172 | p = Pipe[int]{ 173 | Fn: func(i int) (*int, bool) { 174 | return &i, false 175 | }, 176 | Len: 10000, 177 | ValLim: -1, 178 | GoroutinesCnt: 1, 179 | } 180 | require.Equal(t, 10000, p.Count()) 181 | 182 | p = Pipe[int]{ 183 | Fn: func(i int) (*int, bool) { 184 | return &i, false 185 | }, 186 | Len: -1, 187 | ValLim: 10000, 188 | GoroutinesCnt: 7, 189 | } 190 | require.Equal(t, 10000, p.Count()) 191 | 192 | p = Pipe[int]{ 193 | Fn: func(i int) (*int, bool) { 194 | return &i, false 195 | }, 196 | Len: -1, 197 | ValLim: 10000, 198 | GoroutinesCnt: 1, 199 | } 200 | require.Equal(t, 10000, p.Count()) 201 | } 202 | 203 | func Test_limit(t *testing.T) { 204 | t.Parallel() 205 | 206 | p := Pipe[int]{ 207 | Len: 10000, 208 | ValLim: -1, 209 | } 210 | require.Equal(t, 10000, p.limit()) 211 | p = Pipe[int]{ 212 | Len: -1, 213 | ValLim: 10000, 214 | } 215 | require.Equal(t, 10000, p.limit()) 216 | p = Pipe[int]{ 217 | Len: -1, 218 | ValLim: -1, 219 | } 220 | require.Equal(t, math.MaxInt-1, p.limit()) 221 | } 222 | -------------------------------------------------------------------------------- /perf/README.md: -------------------------------------------------------------------------------- 1 | # Performance testing 2 | 3 | All performance tests will be accumulated under the current directory 4 | 5 | # Results of tests by version 6 | 7 | goos: linux 8 | goarch: amd64 9 | go version go1.21.6 linux/amd64 10 | pkg: github.com/koss-null/funcfrog/perf 11 | cpu: AMD Ryzen 7 3800X 8-Core Processor 12 | 13 | ### v1.0.7 14 | BenchmarkMap-16 16 87552984 ns/op 15 | BenchmarkMapParallel-16 62 18736475 ns/op 16 | BenchmarkMapFor-16 31 40721673 ns/op 17 | BenchmarkFilter-16 16 71662353 ns/op 18 | BenchmarkFilterParallel-16 88 11903494 ns/op 19 | BenchmarkFilterFor-16 39 28844513 ns/op 20 | BenchmarkReduce-16 9 120268926 ns/op 21 | BenchmarkSumParallel-16 82 13788655 ns/op 22 | BenchmarkReduceFor-16 12 93317883 ns/op 23 | BenchmarkAny-16 542 1999494 ns/op 24 | BenchmarkAnyParallel-16 5448 220360 ns/op 25 | BenchmarkAnyFor-16 4972 227679 ns/op 26 | BenchmarkFirst-16 531 2237014 ns/op 27 | BenchmarkFirstParallel-16 2438 510214 ns/op 28 | BenchmarkFirstFor-16 5053 247528 ns/op 29 | 30 | ### v1.0.6 31 | BenchmarkMap-16 18 80799140 ns/op 32 | BenchmarkMapParallel-16 55 21173291 ns/op 33 | BenchmarkMapFor-16 28 43118064 ns/op 34 | BenchmarkFilter-16 21 52797349 ns/op 35 | BenchmarkFilterParallel-16 76 13964775 ns/op 36 | BenchmarkFilterFor-16 20 52667354 ns/op 37 | BenchmarkReduce-16 9 123160362 ns/op 38 | BenchmarkSumParallel-16 80 14282359 ns/op 39 | BenchmarkReduceFor-16 12 93715817 ns/op 40 | BenchmarkAny-16 585 1851099 ns/op 41 | BenchmarkAnyFor-16 10000 117664 ns/op 42 | BenchmarkFirst-16 562 1906542 ns/op 43 | BenchmarkFirstParallel-16 1862 598930 ns/op 44 | BenchmarkFirstFor-16 9931 114462 ns/op 45 | 46 | ### v1.0.5 47 | BenchmarkMap-16 16 85842601 ns/op 48 | BenchmarkMapParallel-16 57 21815099 ns/op 49 | BenchmarkMapFor-16 27 40480230 ns/op 50 | BenchmarkFilter-16 21 54987040 ns/op 51 | BenchmarkFilterParallel-16 79 16224400 ns/op 52 | BenchmarkFilterFor-16 21 53677933 ns/op 53 | BenchmarkReduce-16 8 125530795 ns/op 54 | BenchmarkSumParallel-16 75 14727102 ns/op 55 | BenchmarkReduceFor-16 12 105966012 ns/op 56 | BenchmarkAny-16 260 4570193 ns/op 57 | BenchmarkAnyFor-16 10311 112833 ns/op 58 | BenchmarkFirst-16 277 4300988 ns/op 59 | BenchmarkFirstParallel-16 564 2069967 ns/op 60 | BenchmarkFirstFor-16 5031 233127 ns/op 61 | 62 | ### v1.0.4 63 | BenchmarkMap-16 16 90982855 ns/op 64 | BenchmarkMapParallel-16 63 18710128 ns/op 65 | BenchmarkMapFor-16 32 37019210 ns/op 66 | BenchmarkFilter-16 21 52755260 ns/op 67 | BenchmarkFilterParallel-16 73 14857910 ns/op 68 | BenchmarkFilterFor-16 19 53379115 ns/op 69 | BenchmarkReduce-16 8 135833466 ns/op 70 | BenchmarkSumParallel-16 75 14716286 ns/op 71 | BenchmarkReduceFor-16 12 96555695 ns/op 72 | BenchmarkAny-16 277 4535092 ns/op 73 | BenchmarkAnyFor-16 9892 122538 ns/op 74 | BenchmarkFirst-16 271 4347068 ns/op 75 | BenchmarkFirstParallel-16 554 2085338 ns/op 76 | BenchmarkFirstFor-16 5005 236598 ns/op 77 | 78 | ### v1.0.3 79 | BenchmarkMap-16 18 88580142 ns/op 80 | BenchmarkMapParallel-16 57 20461972 ns/op 81 | BenchmarkMapFor-16 26 41769221 ns/op 82 | BenchmarkFilter-16 20 55752069 ns/op 83 | BenchmarkFilterParallel-16 81 16244908 ns/op 84 | BenchmarkFilterFor-16 22 60170064 ns/op 85 | BenchmarkReduce-16 8 126296178 ns/op 86 | BenchmarkSumParallel-16 76 14507451 ns/op 87 | BenchmarkReduceFor-16 12 93559970 ns/op 88 | BenchmarkAny-16 258 4559351 ns/op 89 | BenchmarkAnyFor-16 10132 116675 ns/op 90 | BenchmarkFirst-16 273 4357503 ns/op 91 | BenchmarkFirstParallel-16 572 2071073 ns/op 92 | BenchmarkFirstFor-16 5252 225960 ns/op 93 | 94 | ### v1.0.2 95 | BenchmarkMap-16 16 90675274 ns/op 96 | BenchmarkMapParallel-16 56 21817811 ns/op 97 | BenchmarkMapFor-16 34 43180383 ns/op 98 | BenchmarkFilter-16 21 52800059 ns/op 99 | BenchmarkFilterParallel-16 74 16338336 ns/op 100 | BenchmarkFilterFor-16 21 52904371 ns/op 101 | BenchmarkReduce-16 9 128043828 ns/op 102 | BenchmarkSumParallel-16 74 14941401 ns/op 103 | BenchmarkReduceFor-16 12 106449945 ns/op 104 | BenchmarkAny-16 283 4579511 ns/op 105 | BenchmarkAnyFor-16 9838 118473 ns/op 106 | BenchmarkFirst-16 280 4317570 ns/op 107 | BenchmarkFirstParallel-16 174 6847934 ns/op 108 | BenchmarkFirstFor-16 5247 235376 ns/op 109 | 110 | ### v1.0.1 111 | BenchmarkMap-16 16 90069049 ns/op 112 | BenchmarkMapParallel-16 51 22195910 ns/op 113 | BenchmarkMapFor-16 33 41674366 ns/op 114 | BenchmarkFilter-16 27 52384601 ns/op 115 | BenchmarkFilterParallel-16 85 16269475 ns/op 116 | BenchmarkFilterFor-16 21 53063409 ns/op 117 | BenchmarkReduce-16 9 120605115 ns/op 118 | BenchmarkSumParallel-16 74 14792637 ns/op 119 | BenchmarkReduceFor-16 12 93278907 ns/op 120 | BenchmarkAny-16 260 4675154 ns/op 121 | BenchmarkAnyFor-16 9943 115879 ns/op 122 | BenchmarkFirst-16 295 4331381 ns/op 123 | BenchmarkFirstParallel-16 177 6277064 ns/op 124 | BenchmarkFirstFor-16 5002 228628 ns/op 125 | 126 | -------------------------------------------------------------------------------- /perf/perf_test.go: -------------------------------------------------------------------------------- 1 | package perf_test 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/koss-null/funcfrog/pkg/pipe" 8 | ) 9 | 10 | func fib(_ int) int { 11 | // about 100 operations to get 91th fib number 12 | n := 100 // 100 iters 13 | a, b := 0, 1 14 | for i := 0; i < n; i++ { 15 | a, b = b, a+b 16 | } 17 | return a 18 | } 19 | 20 | // Define the predicate function for the filter operation 21 | func filterFunc(x *int) bool { 22 | // Perform a complex condition check 23 | return fib(*x%2) == 0 24 | } 25 | 26 | // Define the binary function for the reduce operation 27 | func reduceFunc(x, y *int) int { 28 | // Perform a complex calculation or aggregation 29 | return fib(*x)%1024 + fib(*y)%1024 30 | } 31 | 32 | func BenchmarkMap(b *testing.B) { 33 | b.StopTimer() 34 | input := make([]int, 1_000_000) 35 | for i := 0; i < len(input); i++ { 36 | input[i] = i 37 | } 38 | b.StartTimer() 39 | 40 | for j := 0; j < b.N; j++ { 41 | pipe := pipe.Slice(input) 42 | result := pipe.Map(fib).Do() 43 | _ = result 44 | } 45 | } 46 | 47 | func BenchmarkMapParallel(b *testing.B) { 48 | b.StopTimer() 49 | input := make([]int, 1_000_000) 50 | for i := 0; i < len(input); i++ { 51 | input[i] = i 52 | } 53 | b.StartTimer() 54 | 55 | for j := 0; j < b.N; j++ { 56 | pipe := pipe.Slice(input).Parallel(uint16(runtime.NumCPU())) 57 | result := pipe.Map(fib).Do() 58 | _ = result 59 | } 60 | } 61 | 62 | func BenchmarkMapFor(b *testing.B) { 63 | b.StopTimer() 64 | input := make([]int, 1_000_000) 65 | for i := 0; i < len(input); i++ { 66 | input[i] = i 67 | } 68 | b.StartTimer() 69 | 70 | for j := 0; j < b.N; j++ { 71 | result := make([]int, 0, len(input)) 72 | for i := range input { 73 | result = append(result, fib(input[i])) 74 | } 75 | _ = result 76 | } 77 | } 78 | 79 | func BenchmarkFilter(b *testing.B) { 80 | b.StopTimer() 81 | input := make([]int, 1_000_000) 82 | for i := 0; i < len(input); i++ { 83 | input[i] = i 84 | } 85 | b.StartTimer() 86 | 87 | for j := 0; j < b.N; j++ { 88 | pipe := pipe.Slice(input) 89 | result := pipe.Filter(filterFunc).Do() 90 | _ = result 91 | } 92 | } 93 | 94 | func BenchmarkFilterParallel(b *testing.B) { 95 | b.StopTimer() 96 | input := make([]int, 1_000_000) 97 | for i := 0; i < len(input); i++ { 98 | input[i] = i 99 | } 100 | b.StartTimer() 101 | 102 | for j := 0; j < b.N; j++ { 103 | pipe := pipe.Slice(input).Parallel(uint16(runtime.NumCPU())) 104 | result := pipe.Filter(filterFunc).Do() 105 | _ = result 106 | } 107 | } 108 | 109 | func BenchmarkFilterFor(b *testing.B) { 110 | b.StopTimer() 111 | input := make([]int, 1_000_000) 112 | for i := 0; i < len(input); i++ { 113 | input[i] = i 114 | } 115 | b.StartTimer() 116 | 117 | for j := 0; j < b.N; j++ { 118 | result := make([]int, 0) 119 | for i := range input { 120 | if filterFunc(&input[i]) { 121 | result = append(result, input[i]) 122 | } 123 | } 124 | _ = result 125 | } 126 | } 127 | 128 | func BenchmarkReduce(b *testing.B) { 129 | b.StopTimer() 130 | input := make([]int, 1_000_000) 131 | for i := 0; i < len(input); i++ { 132 | input[i] = i 133 | } 134 | b.StartTimer() 135 | 136 | for j := 0; j < b.N; j++ { 137 | pipe := pipe.Slice(input) 138 | result := pipe.Reduce(reduceFunc) 139 | _ = result 140 | } 141 | } 142 | 143 | func BenchmarkSumParallel(b *testing.B) { 144 | b.StopTimer() 145 | input := make([]int, 1_000_000) 146 | for i := 0; i < len(input); i++ { 147 | input[i] = i 148 | } 149 | b.StartTimer() 150 | 151 | for j := 0; j < b.N; j++ { 152 | pipe := pipe.Slice(input).Parallel(uint16(runtime.NumCPU())) 153 | result := pipe.Sum(reduceFunc) 154 | _ = result 155 | } 156 | } 157 | 158 | // Benchmark the reduce with for-loop 159 | func BenchmarkReduceFor(b *testing.B) { 160 | b.StopTimer() 161 | input := make([]int, 1_000_000) 162 | for i := 0; i < len(input); i++ { 163 | input[i] = i 164 | } 165 | b.StartTimer() 166 | 167 | for j := 0; j < b.N; j++ { 168 | result := 0 169 | for i := range input { 170 | result = reduceFunc(&result, &input[i]) 171 | } 172 | _ = result 173 | } 174 | } 175 | 176 | func BenchmarkAny(b *testing.B) { 177 | b.StopTimer() 178 | input := make([]int, 1_000_000) 179 | for i := 0; i < len(input); i++ { 180 | input[i] = i 181 | } 182 | b.StartTimer() 183 | 184 | for j := 0; j < b.N; j++ { 185 | pipe := pipe.Slice(input).Filter(func(x *int) bool { return *x > 5_000_00 }) 186 | result := pipe.Any() 187 | _ = result 188 | } 189 | } 190 | 191 | func BenchmarkAnyParallel(b *testing.B) { 192 | b.StopTimer() 193 | input := make([]int, 1_000_000) 194 | for i := 0; i < len(input); i++ { 195 | input[i] = i 196 | } 197 | b.StartTimer() 198 | 199 | for j := 0; j < b.N; j++ { 200 | pipe := pipe.Slice(input). 201 | Parallel(uint16(runtime.NumCPU())). 202 | Filter(func(x *int) bool { return *x > 5_000_00 }) 203 | result := pipe.Any() 204 | _ = result 205 | } 206 | } 207 | 208 | func BenchmarkAnyFor(b *testing.B) { 209 | b.StopTimer() 210 | input := make([]int, 1_000_000) 211 | for i := 0; i < len(input); i++ { 212 | input[i] = i 213 | } 214 | b.StartTimer() 215 | 216 | for j := 0; j < b.N; j++ { 217 | for i := 0; i < len(input); i++ { 218 | if input[i] > 5_000_00 { 219 | _ = input[i] 220 | break 221 | } 222 | } 223 | } 224 | } 225 | 226 | func BenchmarkFirst(b *testing.B) { 227 | b.StopTimer() 228 | input := make([]int, 1_000_000) 229 | for i := 0; i < len(input); i++ { 230 | input[i] = i 231 | } 232 | b.StartTimer() 233 | 234 | for j := 0; j < b.N; j++ { 235 | pipe := pipe.Slice(input).Filter(func(x *int) bool { return *x > 5_000_00 }) 236 | result := pipe.First() 237 | _ = result 238 | } 239 | } 240 | 241 | func BenchmarkFirstParallel(b *testing.B) { 242 | b.StopTimer() 243 | input := make([]int, 1_000_000) 244 | for i := 0; i < len(input); i++ { 245 | input[i] = i 246 | } 247 | b.StartTimer() 248 | 249 | for j := 0; j < b.N; j++ { 250 | pipe := pipe.Slice(input). 251 | Parallel(uint16(runtime.NumCPU())). 252 | Filter(func(x *int) bool { return *x > 5_000_00 }) 253 | result := pipe.First() 254 | _ = result 255 | } 256 | } 257 | 258 | func BenchmarkFirstFor(b *testing.B) { 259 | b.StopTimer() 260 | input := make([]int, 1_000_000) 261 | for i := 0; i < len(input); i++ { 262 | input[i] = i 263 | } 264 | b.StartTimer() 265 | 266 | for j := 0; j < b.N; j++ { 267 | for i := 0; i < len(input); i++ { 268 | if input[i] > 5_000_00 { 269 | _ = input[i] 270 | break 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FuncFrog 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/koss-null/funcfrog)](https://goreportcard.com/report/github.com/koss-null/funcfrog) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/koss-null/funcfrog.svg)](https://pkg.go.dev/github.com/koss-null/funcfrog) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![Coverage](https://raw.githubusercontent.com/koss-null/funcfrog/master/coverage.svg?raw=true)](coverage.svg) 7 | 8 | ![FuncFrog icon](https://github.com/koss-null/funcfrog/blob/master/FuncFrogIco.jpg?raw=true) 9 | 10 | *FuncFrog* is a library for performing **efficient**, **parallel**, **lazy** `map`, `reduce`, `filter` and [many other](#supported-functions-list) operations on slices and other data sequences in a pipeline. The sequence can be set by a variety of [generating functions](#constructors). Everything is supported to be executed in parallel with **minimal overhead** on copying and locks. There is a built-in support of [error handling](#error-handling) with Yeet/Snag methods 11 | The library is easy to use and has a clean, intuitive API. 12 | You can measure performance comparing to vanilla `for` loop on your machine using `cd perf/; make` (spoiler: FuncFrog 13 | is better when multithreading). 14 | 15 | ## Table of Contents 16 | - [Getting Started](#getting-started) 17 | - [Basic information](#basic-information) 18 | - **[Supported functions list](#supported-functions-list)** 19 | - [Constructors](#constructors) 20 | - [Set Pipe length](#set-pipe-length) 21 | - [Split evaluation into *n* goroutines](#split-evaluation-into-n-goroutines) 22 | - [Transform data](#transform-data) 23 | - [Retrieve a single element or perform a boolean check](#retrieve-a-single-element-or-perform-a-boolean-check) 24 | - [Evaluate the pipeline](#evaluate-the-pipeline) 25 | - [Transform Pipe *from one type to another*](#transform-pipe-from-one-type-to-another) 26 | - [Easy type conversion for Pipe[any]]( #easy-type-conversion-for-pipe[any]) 27 | - [Error handling](#error-handling) 28 | - [To be done](#to-be-done) 29 | - [Using prefix `Pipe` to transform `Pipe` type](#using-prefix-pipe-to-transform-pipe-type) 30 | - [Using `ff` package to write shortened pipes](#using-ff-package-to-write-shortened-pipes) 31 | - [Look for useful functions in `Pipies` package](#look-for-useful-functions-in-pipies-package) 32 | - **[Examples](#examples)** 33 | - [Basic example](#basic-example) 34 | - [Example using `Func` and `Take`](#example-using-func-and-take) 35 | - [Example using `Func` and `Gen`](#example-using-func-and-gen) 36 | - [Example difference between `Take` and `Gen`](#example-difference-between-take-and-gen) 37 | - [Example using `Filter` and `Map`](#example-using-filter-and-map) 38 | - [Example using `Map` and `Reduce`](#example-using-map-and-reduce) 39 | - [Example of `Map` and `Reduce` with the underlying array type change](#example-of-map-and-reduce-with-the-underlying-array-type-change) 40 | - [Example using `Sort`](#example-using-sort) 41 | - [Example of infine sequence generation](#example-of-infine-sequence-generation) 42 | - [Example using `Range` and `Map`](#example-using-range-not-implemented-yet-and-map) 43 | - [Example using `Repeat` and `Map`](#example-using-repeat-not-implemented-yet-and-map) 44 | - [Example using `Cycle` and `Filter`](#example-using-cycle-not-implemented-yet-and-filter) 45 | - [Example using `Erase` and `Collect`](#example-using-erase-and-collect) 46 | - [Example of simple error handling](#example-of-simple-error-handling) 47 | - [Example of multiple error handling](example-of-multiple-error-handling) 48 | - [Is this package stable?](#is-this-package-stable) 49 | - [Contributions](#contributions) 50 | - [What's next?](#whats-next) 51 | 52 | ## Getting Started 53 | 54 | To use FuncFrog in your project, run the following command: 55 | 56 | ``` 57 | go get github.com/koss-null/funcfrog 58 | ``` 59 | 60 | Then, import the library into your Go code (basically you need the pipe package): 61 | 62 | ```go 63 | import "github.com/koss-null/funcfrog/pkg/pipe" 64 | ``` 65 | 66 | You can then use the `pipe` package to create a pipeline of operations on a slice: 67 | ```go 68 | res := pipe.Slice(a). 69 | Map(func(x int) int { return x * x }). 70 | Filter(func(x *int) bool { return *x > 100 }). 71 | Parallel(12). 72 | Do() 73 | ``` 74 | 75 | All operations are carefully fenced with interfaces, so feel free to use anything, autosuggestion suggests you. 76 | 77 | If you want it fast and short, you may use `ff`: 78 | ```go 79 | import "github.com/koss-null/funcfrog/pkg/ff" 80 | 81 | res := ff.Map(strArr, strings.ToUpper).Do() 82 | ``` 83 | 84 | To see some code snippets, check out the [Examples](#examples). 85 | 86 | ## Basic information 87 | 88 | The `Piper` (or `PiperNoLen` for pipes with undetermined lengths) is an interface that represents a *lazy-evaluated sequence of data*. The `Piper` interface provides a set of methods that can be used to transform, filter, collect and analyze data in the sequence. 89 | Every pipe can be conveniently copied at every moment just by equating it to a variable. Some methods (as `Take` or `Gen`) lead from `PiperNoLen` to `Piper` interface making wider method range available. 90 | 91 | ## Supported functions list 92 | 93 | The following functions can be used to create a new `Pipe` (this is how I call the inner representation of a sequence ofelements and a sequence operations on them): 94 | #### Constructors 95 | - :frog: `Slice([]T) Piper`: creates a `Pipe` of a given type `T` from a slice, *the length is known*. 96 | - :frog: `Func(func(i int) (T, bool)) PiperNL`: creates a `Pipe` of type `T` from a function. The function returns an element which is considered to be at `i`th position in the `Pipe`, as well as a boolean indicating whether the element should be included (`true`) or skipped (`false`), *the length is unknown*. 97 | - :frog: `Fn(func(i int) (T)) PiperNL`: creates a `Pipe` of type `T` from a function. The function should return the value of the element at the `i`th position in the `Pipe`; to be able to skip values use `Func`. 98 | - :frog: `FuncP(func(i int) (*T, bool)) PiperNL`: creates a `Pipe` of type `T` from a function. The function returns a pointer to an element which is considered to be at `i`th position in the `Pipe`, as well as a boolean indicating whether the element should be included (`true`) or skipped (`false`), *the length is unknown*. 99 | - :frog: `Cycle(data []T) PiperNL`: creates a new `Pipe` that cycles through the elements of the provided slice indefinitely. *The length is unknown.* 100 | - :frog: `Range(start, end, step T) Piper`: creates a new `Pipe` that generates a sequence of values of type `T` from `start` to `end` (exclusive) with a fixed `step` value between each element. `T` can be any numeric type, such as `int`, `float32`, or `float64`. *The length is known.* 101 | - :frog: `Repeat(x T, n int) Piper`: creates a new `Pipe` that generates a sequence of values of type `T` and value x with the length of n. *The length is known.* 102 | 103 | #### Set Pipe length 104 | - :frog: `Take(n int) Piper`: if it's a `Func`-made `Pipe`, expects `n` values to be eventually returned. *Transforms unknown length to known.* 105 | - :frog: `Gen(n int) Piper`: if it's a `Func`-made `Pipe`, generates a sequence from `[0, n)` and applies the function to it. *Transforms unknown length to known.* 106 | 107 | 108 | #### Split evaluation into *n* goroutines 109 | - :frog: `Parallel(n int) Pipe`: sets the number of goroutines to be executed on (1 by default). This function can be used to specify the level of parallelism in the pipeline. *Availabble for unknown length.* 110 | 111 | #### Transform data 112 | - :frog: `Map(fn func(x T) T) Pipe`: applies the function `fn` to every element of the `Pipe` and returns a new `Pipe` with the transformed data. *Available for unknown length.* 113 | - :frog: `Filter(fn func(x *T) bool) Pipe`: applies the predicate function `fn` to every element of the `Pipe` and returns a new `Pipe` with only the elements that satisfy the predicate. *Available for unknown length.* 114 | - :frog: `MapFilter(fn func(T) (T, bool)) Piper[T]`: applies given function to each element of the underlying slice. If the second returning value of `fn` is *false*, the element is skipped (may be **useful for error handling**). 115 | - :frog: `Reduce(fn func(x, y *T) T) *T`: applies the binary function `fn` to the elements of the `Pipe` and returns a single value that is the result of the reduction. Returns `nil` if the `Pipe` was empty before reduction. 116 | - :frog: `Sum(plus func(x, y *T) T) T`: makes parallel reduce with associative function `plus`. 117 | - :frog: `Sort(less func(x, y *T) bool) Pipe`: sorts the elements of the `Pipe` using the provided `less` function as the comparison function. 118 | 119 | #### Retrieve a single element or perform a boolean check 120 | - :frog: `Any() T`: returns a random element existing in the pipe. *Available for unknown length.* 121 | - :frog: `First() T`: returns the first element of the `Pipe`, or `nil` if the `Pipe` is empty. *Available for unknown length.* 122 | - :frog: `Count() int`: returns the number of elements in the `Pipe`. It does not allocate memory for the elements, but instead simply returns the number of elements in the `Pipe`. 123 | 124 | #### Evaluate the pipeline 125 | - :frog: `Do() []T` function is used to **execute** the pipeline and **return the resulting slice of data**. This function should be called at the end of the pipeline to retrieve the final result. 126 | 127 | #### Transform Pipe *from one type to another* 128 | - :frog: `Erase() Pipe[any]`: returns a pipe where all objects are the objects from the initial `Pipe` but with erased type. Basically for each `x` it returns `any(&x)`. Use `pipe.Collect[T](Piper[any]) PiperT` to collect it back into some type (or `pipe.CollectNL` for slices with length not set yet). 129 | 130 | #### Easy type conversion for Pipe[any] 131 | - :frog: `pipe.Collect[T](Piper[any]) PiperNoLen[T]` 132 | - :frog: `pipe.CollectNL[T](PiperNoLen[any]) PiperNoLen[T]` 133 | This functions takes a Pipe of erased `interface{}` type (which is pretty useful if you have a lot of type conversions along your pipeline and can be achieved by calling `Erase()` on a `Pipe`). Basically, for each element `x` in a sequence `Collect` returns `*(x.(*T))` element. 134 | 135 | #### Error handling 136 | - :frog: `Yeti(yeti) Pipe[T]`:set a `yeti` - an object that will collect errors thrown with `yeti.Yeet(error)` and will be used to handle them. 137 | - :frog: `Snag(func(error)) Pipe[T]`: set a function that will handle all errors which have been sent with `yeti.Yeet(error)` to the **last** `yeti` object that was set through `Pipe[T].Yeti(yeti) Pipe[T]` method. 138 | Error handling may look pretty uncommon at a first glance. To get better intuition about it you may like to check out [examples](#example-of-simple-error-handling) section. 139 | 140 | #### To be done 141 | - :seedling: TBD: `Until(fn func(*T) bool)`: if it's a `Func`-made `Pipe`, it evaluates one-by-one until fn return false. *This feature may require some new `Pipe` interfaces, since it is applicable only in a context of a single thread* 142 | - :seedling: *TBD*: `IsAny() bool`: returns `true` if the `Pipe` contains any elements, and `false` otherwise. *Available for unknown length.* 143 | - :seedling: *TBD*: `MoreThan(n int) bool`: returns `true` if the `Pipe` contains more than `n` elements, and `false` otherwise. *Available for unknown length.* 144 | - :seedling: *TBD*: `Reverse() *Pipe`: reverses the underlying slice. 145 | 146 | In addition to the functions described above, the `pipe` package also provides several utility functions that can be used to create common types of `Pipe`s, such as `Range`, `Repeat`, and `Cycle`. These functions can be useful for creating `Pipe`s of data that follow a certain pattern or sequence. 147 | 148 | Also it is highly recommended to get familiarize with the `pipies` package, containing some useful *predecates*, *comparators* and *accumulators*. 149 | 150 | ### Using prefix `Pipe` to transform `Pipe` type 151 | 152 | You may found that using `Erase()` is not so convenient as it makes you to do some pointer conversions. Fortunately there is another way to convert a pipe type: use functions from `pipe/prefixpipe.go`. These functions takes `Piper` or `PiperNoLen` as a first parameter and function to apply as the second and returns a resulting pipe (or the result itself) of a destination type. 153 | 154 | #### Prefix pipe functinos 155 | 156 | - :frog: `pipe.Map(Piper[SrcT], func(x SrcT) DstT) Piper[DstT] ` - applies *map* from one type to another for the `Pipe` with **known** length. 157 | - :frog: `pipe.MapNL(PiperNoLen[SrcT], func(x SrcT) DstT) PiperNoLen[DstT] ` - applies *map* from one type to another for the `Pipe` with **unknown** length. 158 | - :frog: `Reduce(Piper[SrcT], func(*DstT, *SrcT) DstT, initVal ...DstT)` - applies *reduce* operation on `Pipe` of type `SrcT` and returns result of type `DstT`. `initVal` is an optional parameter to **initialize** a value that should be used on the **first steps** of reduce. 159 | 160 | ### Using `ff` package to write shortened pipes 161 | 162 | Sometimes you need just to apply a function. Creating a pipe using `pipe.Slice` and then call `Map` looks a little bit verbose, especially when you need to call `Map` or `Reduce` from one type to another. The solution for it is `funcfrog/pkg/ff` package. It contains shortened `Map` and `Reduce` functions which can be called directly with a slice as a first parameter. 163 | 164 | - :frog: `Map([]SrcT, func(SrcT) DstT) pipe.Piper[DstT]` - applies sent function to a slice, returns a `Pipe` of resulting type 165 | - :frog: `Reduce([]SrcT, func(*DstT, *SrcT) DstT, initVal ...DstT) DstT` - applies *reduce* operation on a slice and returns the result of type `DstT`. `initVal` is an optional parameter to **initialize** a value that should be used on the **first steps** of reduce. 166 | 167 | ### Look for useful functions in `Pipies` package 168 | 169 | Some of the functions that are sent to `Map`, `Filter` or `Reduce` (or other `Pipe` methods) are pretty common. Also there is a common comparator for any integers and floats for a `Sort` method. 170 | 171 | ## Examples 172 | 173 | ### Basic example: 174 | 175 | ```go 176 | res := pipe.Slice(a). 177 | Map(func(x int) int { return x * x }). 178 | Map(func(x int) int { return x + 1 }). 179 | Filter(func(x *int) bool { return *x > 100 }). 180 | Filter(func(x *int) bool { return *x < 1000 }). 181 | Parallel(12). 182 | Do() 183 | ``` 184 | 185 | ### Example using `Func` and `Take`: 186 | 187 | ```go 188 | p := pipe.Func(func(i int) (v int, b bool) { 189 | if i < 10 { 190 | return i * i, true 191 | }; return 192 | }).Take(5).Do() 193 | // p will be [0, 1, 4, 9, 16] 194 | ``` 195 | 196 | ### Example using `Func` and `Gen`: 197 | 198 | ```go 199 | p := pipe.Func(func(i int) (v int, b bool) { 200 | if i < 10 { 201 | return i * i, true 202 | }; return 203 | }).Gen(5).Do() 204 | // p will be [0, 1, 4, 9, 16] 205 | ``` 206 | 207 | ### Example difference between `Take` and `Gen`: 208 | 209 | Gen(n) generates the sequence of n elements and applies all pipeline afterwards. 210 | ```go 211 | 212 | p := pipe.Func(func(i int) (v int, b bool) { 213 | return i, true 214 | }). 215 | Filter(func(x *int) bool { return (*x) % 2 == 0}) 216 | Gen(10). 217 | Do() 218 | // p will be [0, 2, 4] 219 | ``` 220 | 221 | Take(n) expects the result to be of n length. 222 | ```go 223 | p := pipe.Func(func(i int) (v int, b bool) { 224 | return i, true 225 | }). 226 | Filter(func(x *int) bool { return (*x) % 2 == 0}) 227 | Take(10). 228 | Do() 229 | // p will be [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] 230 | ``` 231 | 232 | Watch out, if Take value is set uncarefully, it may jam the whole pipenile. 233 | ```go 234 | // DO NOT DO THIS, IT WILL JAM 235 | p := pipe.Func(func(i int) (v int, b bool) { 236 | return i, i < 10 // only 10 first values are not skipped 237 | }). 238 | Take(11). // we can't get any 11th value ever 239 | Parallel(4). // why not 240 | Do() 241 | // Do() will try to evaluate the 11th value in 4 goroutines until it reaches maximum int value 242 | ``` 243 | 244 | ### Example using `Filter` and `Map`: 245 | 246 | ```go 247 | p := pipe.Slice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}). 248 | Filter(func(x *int) bool { return *x % 2 == 0 }). 249 | Map(func(x int) int { return len(strconv.Itoa(x)) }). 250 | Do() 251 | // p will be [1, 1, 1, 1, 2] 252 | ``` 253 | 254 | ### Example using `Map` and `Reduce`: 255 | 256 | In this example Reduce is used in it's prefix form to be able to convert ints to string. 257 | ```go 258 | p := pipe.Reduce( 259 | pipe.Slice([]int{1, 2, 3, 4, 5}). 260 | Map(func(x int) int { return x * x }), 261 | func(x, y *int) string { 262 | return strconv.Itoa(*x) + "-" + strconv.Itoa(y) 263 | }, 264 | ) 265 | // p will be "1-4-9-16-25" 266 | ``` 267 | 268 | In this example Reduce is used as usual in it's postfix form. 269 | ```go 270 | p := pipe.Slice([]stirng{"Hello", "darkness", "my", "old", "friend"}). 271 | Map(strings.Title). 272 | Reduce(func(x, y *string) string { 273 | return *x + " " + *y 274 | }) 275 | ) 276 | // p will be "Hello Darkness My Old Friend" 277 | ``` 278 | 279 | ### Example of `Map` and `Reduce` with the underlying array type change: 280 | 281 | ```go 282 | p := pipe.Slice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}) 283 | strP := pipe.Map(p, func(x int) string { return strconv.Itoa(x) }) 284 | result := pipe.Reduce(strP, func(x, y *string) int { return len(*x) + len(*y) }).Do() 285 | // result will be 45 286 | ``` 287 | 288 | ### Example using `Sort`: 289 | 290 | ```go 291 | p := pipe.Func(func(i int) (float32, bool) { 292 | return 100500-float32(i) * 0.9, true 293 | }). 294 | Map(func(x float32) float32 { return x * x * 0.1 }). 295 | Gen(100500). // Sort is only availavle on pipes with known length 296 | Sort(pipies.Less[float32]). // pipies.Less(x, y *T) bool is available to all comparables 297 | // check out pipies package to find more usefull things 298 | Parallel(12). 299 | Do() 300 | // p will contain the elements sorted in ascending order 301 | ``` 302 | 303 | ### Example of infine sequence generation: 304 | 305 | Here is an example of generating an infinite sequence of Fibonacci: 306 | 307 | ```go 308 | var fib []chan int 309 | p := pipe.Func(func(i int) (int, bool) { 310 | if i < 2 { 311 | fib[i] <- i 312 | return i, true 313 | } 314 | p1 := <-fib[i-1]; fib[i-1] <- p1 315 | p2 := <-fib[i-2]; fib[i-2] <- p2 316 | 317 | fib[i] <- p1 + p2 318 | return p1 + p2, true 319 | }).Parallel(20) 320 | ``` 321 | 322 | To generate a specific number of values, you can use the `Take` or `Gen` method: 323 | 324 | ```go 325 | // fill the array first 326 | fib = make([]chan int, 60) 327 | for i := range fib { fib[i] = make(chan int, 1) } 328 | // do the Take 329 | p = p.Take(60) 330 | ``` 331 | 332 | To accumulate the elements of the `Pipe`, you can use the `Reduce` or `Sum` method: 333 | 334 | ```go 335 | sum := p.Sum(pipe.Sum[float32]) 336 | //also you can: sum := p.Reduce(func(x, y *float32) float32 { return *x + *y}) 337 | // sum will be the sum of the first 65000 random float32 values greater than 0.5 338 | ``` 339 | 340 | ### Example using `Range` and `Map`: 341 | 342 | ```go 343 | p := pipe.Range(10, 20, 2).Map(func(x int) int { return x * x }).Do() 344 | // p will be [100, 144, 196, 256, 324] 345 | ``` 346 | 347 | ### Example using `Repeat` and `Map`: 348 | 349 | ```go 350 | p := pipe.Repeat("hello", 5).Map(strings.ToUpper).Do() 351 | // p will be ["HELLO", "HELLO", "HELLO", "HELLO", "HELLO"] 352 | ``` 353 | Here is an example how you can handle multiple function returning error call this way: 354 | 355 | ```go 356 | func foo() error { 357 | // <...> 358 | return nil 359 | } 360 | 361 | errs := pipe.Map( 362 | pipe.Repeat(foo, 50), 363 | func(f func() error) error { return f() }, 364 | ).Do() 365 | 366 | for _, e := range errs { 367 | if e != nil { 368 | log.Err(e) 369 | } 370 | } 371 | ``` 372 | 373 | ### Example using `Cycle` and `Filter` 374 | 375 | ```go 376 | p := pipe.Cycle([]int{1, 2, 3}).Filter(func(x *int) bool { return *x % 2 == 0 }).Take(4).Do() 377 | // p will be [2, 2, 2, 2] 378 | ``` 379 | 380 | ### Example using `Erase` and `Collect` 381 | 382 | ```go 383 | p := pipe.Slice([]int{1, 2, 3}). 384 | Erase(). 385 | Map(func(x any) any { 386 | i := *(x.(*int)) 387 | return &MyStruct{Weight: i} 388 | }).Filter(x *any) bool { 389 | return (*x).(*MyStruct).Weight > 10 390 | } 391 | ms := pipe.Collect[MyStruct](p).Parallel(10).Do() 392 | ``` 393 | 394 | 395 | 396 | ### Example of simple error handling 397 | 398 | ```go 399 | y := pipe.NewYeti() 400 | p := pipe.Range[int](-10, 10, 1). 401 | Yeti(y). // it's important to set yeti before yeeting, or the handle process will not be called 402 | MapFilter(func(x int) (int, bool) { 403 | if x == 0 { 404 | y.Yeet(errors.New("zero devision")) // yeet the error 405 | return 0, false // use MapFilter to filter out this value 406 | } 407 | return int(256.0 / float64(x)), true 408 | }).Snag(func(err error) { 409 | fmt.Println("oopsie-doopsie: ", err) 410 | }).Do() 411 | 412 | fmt.Println("p is: ", p) 413 | /////////// output is: 414 | // oopsie-doopsie: zero devision 415 | // p is: [-25 -28 -32 -36 -42 -51 -64 -85 -128 -256 256 128 85 64 51 42 36 32 28] 416 | ``` 417 | 418 | This example demonstrates generating a set of values 256/i, where i ranges from -10 to 9 (excluding 10) with a step of 1. To handle division by zero, the library provides an error handling mechanism. 419 | 420 | To begin, you need to create an error handler using the `pipe.NewYeti()` function. Then, register the error handler by calling the `Yeti(yeti)` method on your `pipe` object. This registered `yeti` will be the **last** error handler used in the `pipe` chain. 421 | 422 | To **yeet** an error, you can use `y.Yeet(error)` from the registered `yeti` object. 423 | 424 | To **handle** the yeeted error, use the `Snag(func(error))` method, which sets up an error handling function. You can set up multiple `Snag` functions, but all of them will consider the last `yeti` object set with the `Yeti(yeti)` method. 425 | 426 | This is a simple example of how to handle basic errors. Below, you will find a more realistic example of error handling in a real-life scenario. 427 | 428 | ### Example of multiple error handling 429 | 430 | ```go 431 | y1, y2 := pipe.NewYeti(), pipe.NewYeti() 432 | users := pipe.Func(func(i int) (*domain.DomObj, bool) { 433 | domObj, err := uc.GetUser(i) 434 | if err != nil { 435 | y1.Yeet(err) 436 | return nil, false 437 | } 438 | return domObj, true 439 | }). 440 | Yeti(y1).Snag(handleGetUserErr). // suppose we have some pre-defined handler 441 | MapFilter(func(do *domain.DomObj) (*domain.DomObj, bool) { 442 | enriched, err := uc.EnrichUser(do) 443 | if err != nil { 444 | return nil, false 445 | } 446 | return enriched, true 447 | }).Yeti(y2).Snag(handleEnrichUserErr). 448 | Do() 449 | ``` 450 | 451 | The full working code with samples of handlers and implementations of usecase functions can be found at: https://go.dev/play/p/YGtM-OeMWqu. 452 | 453 | 454 | This example demonstrates how multiple error handling functions can be set up at different stages of the data processing pipeline to handle errors specific to each stage. 455 | 456 | Lets break down what is happening here. 457 | 458 | In this code fragment, there are two instances of `pipe.Yeti` created: `y1` and `y2`. These `Yeti` instances are used to handle errors at different stages of the data processing pipeline. 459 | 460 | Within the `pipe.Func` operation, there are error-handling statements. When calling `uc.GetUser(i)`, if an error occurs, it is *yeeted* using `y1.Yeet(err)`, and the function returns `nil` and `false` to indicate the failure. 461 | 462 | The `Yeti(y1).Snag(handleGetUserErr)` statement sets up an error handling function `handleGetUserErr` to handle the error thrown by `uc.GetUser(i)`. This function is defined elsewhere and specifies how to handle the error. 463 | 464 | After that, the `MapFilter` operation is performed on the resulting `*domain.DomObj`. If the `uc.EnrichUser(do)` operation encounters an error, it returns `nil` and `false` to filter out the value. 465 | 466 | The `Yeti(y2).Snag(handleEnrichUserErr)` statement sets up another error handling function `handleEnrichUserErr` to handle the error thrown by `uc.EnrichUser(do)`. 467 | 468 | Finally, the `Do()` method executes the entire pipeline and assigns the result to the `users` variable. 469 | 470 | 471 | ## Is this package stable? 472 | 473 | Yes it finally is **stable since v1.0.0**! All listed functionality is **fully covered** by unit-tests. 474 | Functionality marked as TBD will be implemented as it described in the README and covered by unit-tests to be delivered stable. 475 | 476 | If there will be any method **signature changes**, the **major version** will be incremented. 477 | 478 | ## Contributions 479 | 480 | I will accept any pr's with the functionality marked as TBD. 481 | 482 | Also I will accept any sane unit-tests. 483 | 484 | Bugfixes. 485 | 486 | You are welcome to create any issues and connect to me via email. 487 | 488 | ## What's next? 489 | 490 | I hope to provide some roadmap of the project soon. 491 | 492 | Feel free to fork, inspire and use! 493 | -------------------------------------------------------------------------------- /pkg/pipe/pipe_test.go: -------------------------------------------------------------------------------- 1 | package pipe_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/koss-null/funcfrog/internal/primitive/pointer" 14 | "github.com/koss-null/funcfrog/pkg/ff" 15 | "github.com/koss-null/funcfrog/pkg/pipe" 16 | "github.com/koss-null/funcfrog/pkg/pipies" 17 | ) 18 | 19 | // testing collectors 20 | 21 | func TestCollect(t *testing.T) { 22 | t.Parallel() 23 | 24 | p := pipe.Func(func(i int) (int, bool) { 25 | if i == 4 { 26 | return 0, false 27 | } 28 | return i, true 29 | }).Take(5).Erase() 30 | c := pipe.Collect[int](p).Do() 31 | require.Equal(t, []int{0, 1, 2, 3, 5}, c) 32 | } 33 | 34 | func TestCollectNL(t *testing.T) { 35 | t.Parallel() 36 | 37 | p := pipe.Func(func(i int) (int, bool) { 38 | if i == 4 { 39 | return 0, false 40 | } 41 | return i, true 42 | }).Erase() 43 | c := pipe.CollectNL[int](p).Take(5).Do() 44 | require.Equal(t, []int{0, 1, 2, 3, 5}, c) 45 | } 46 | 47 | // testing constructions 48 | 49 | func TestSlice(t *testing.T) { 50 | t.Parallel() 51 | 52 | cases := []struct { 53 | name string 54 | testCase func() []int 55 | expected func() []int 56 | }{ 57 | { 58 | name: "simple", 59 | testCase: func() []int { 60 | return pipe.Slice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}).Do() 61 | }, 62 | expected: wrap([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}), 63 | }, 64 | { 65 | name: "large", 66 | testCase: func() []int { 67 | return pipe.Slice(largeSlice()).Do() 68 | }, 69 | expected: wrap(largeSlice()), 70 | }, 71 | { 72 | name: "empty", 73 | testCase: func() []int { 74 | return pipe.Slice([]int{}).Do() 75 | }, 76 | expected: wrap([]int{}), 77 | }, 78 | { 79 | name: "single element", 80 | testCase: func() []int { 81 | return pipe.Slice([]int{1}).Do() 82 | }, 83 | expected: wrap([]int{1}), 84 | }, 85 | { 86 | name: "simple parallel 12", 87 | testCase: func() []int { 88 | return pipe.Slice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}).Parallel(10).Do() 89 | }, 90 | expected: wrap([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}), 91 | }, 92 | { 93 | name: "large parallel 12", 94 | testCase: func() []int { 95 | return pipe.Slice(largeSlice()).Parallel(12).Do() 96 | }, 97 | expected: wrap(largeSlice()), 98 | }, 99 | { 100 | name: "empty parallel 12", 101 | testCase: func() []int { 102 | return pipe.Slice([]int{}).Parallel(12).Do() 103 | }, 104 | expected: wrap([]int{}), 105 | }, 106 | { 107 | name: "single element parallel 12", 108 | testCase: func() []int { 109 | return pipe.Slice([]int{1}).Parallel(12).Do() 110 | }, 111 | expected: wrap([]int{1}), 112 | }, 113 | } 114 | 115 | for _, c := range cases { 116 | c := c 117 | t.Run(c.name, func(t *testing.T) { 118 | t.Parallel() 119 | require.Equal(t, c.expected(), c.testCase()) 120 | }) 121 | } 122 | } 123 | 124 | func TestFunc(t *testing.T) { 125 | t.Parallel() 126 | ls := largeSlice() 127 | 128 | cases := []struct { 129 | name string 130 | testCase pipe.Piper[int] 131 | expected func() []int 132 | }{ 133 | { 134 | name: "Simple_gen", 135 | testCase: pipe.Func(func(i int) (int, bool) { 136 | return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}[i], true 137 | }).Gen(10), 138 | expected: wrap([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}), 139 | }, 140 | { 141 | name: "Large_gen", 142 | testCase: pipe.Func(func(i int) (int, bool) { 143 | return ls[i], true 144 | }).Gen(len(ls)), 145 | expected: wrap(largeSlice()), 146 | }, 147 | { 148 | name: "Empty_gen", 149 | testCase: pipe.Func(func(i int) (int, bool) { 150 | return 0, false 151 | }).Gen(0), 152 | expected: wrap([]int{}), 153 | }, 154 | { 155 | name: "Single_element_gen", 156 | testCase: pipe.Func(func(i int) (int, bool) { 157 | return 1, true 158 | }).Gen(1), 159 | expected: wrap([]int{1}), 160 | }, 161 | /////////TAKE 162 | { 163 | name: "Simple_take", 164 | testCase: pipe.Func(func(i int) (int, bool) { 165 | return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}[i], true 166 | }).Take(10), 167 | expected: wrap([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}), 168 | }, 169 | { 170 | name: "Large_take", 171 | testCase: pipe.Func(func(i int) (int, bool) { 172 | return ls[i], true 173 | }).Take(len(ls)), 174 | expected: wrap(largeSlice()), 175 | }, 176 | { 177 | name: "Empty_take", 178 | testCase: pipe.Func(func(i int) (int, bool) { 179 | return 0, false 180 | }).Take(0), 181 | expected: wrap([]int{}), 182 | }, 183 | { 184 | name: "Single_element_take", 185 | testCase: pipe.Func(func(i int) (int, bool) { 186 | return 1, true 187 | }).Take(1), 188 | expected: wrap([]int{1}), 189 | }, 190 | } 191 | 192 | for _, c := range cases { 193 | c := c 194 | t.Run(c.name, func(t *testing.T) { 195 | t.Parallel() 196 | require.Equal(t, c.expected(), c.testCase.Do()) 197 | }) 198 | t.Run(c.name+" parallel 12", func(t *testing.T) { 199 | t.Parallel() 200 | require.Equal(t, c.expected(), c.testCase.Do()) 201 | }) 202 | } 203 | } 204 | 205 | func TestFuncP(t *testing.T) { 206 | t.Parallel() 207 | 208 | const testSize = 10_000 209 | p := pipe.FuncP(func(i int) (*int, bool) { return &i, true }).Gen(testSize).Do() 210 | for i := 0; i < testSize; i++ { 211 | require.Equal(t, i, p[i]) 212 | } 213 | } 214 | 215 | func TestCycle(t *testing.T) { 216 | t.Parallel() 217 | 218 | const testSize = 10_000 219 | c := pipe.Cycle([]int{0, 1, 2, 3, 4}).Take(testSize).Do() 220 | for i := 0; i < testSize; i++ { 221 | require.Equal(t, i%5, c[i]) 222 | } 223 | } 224 | 225 | func TestRange(t *testing.T) { 226 | t.Parallel() 227 | 228 | t.Run("asc", func(t *testing.T) { 229 | t.Parallel() 230 | 231 | r := pipe.Range(0, 10_000, 5).Do() 232 | idx := 0 233 | for i := 0; i < 10_000; i += 5 { 234 | require.Equal(t, i, r[idx]) 235 | idx++ 236 | } 237 | }) 238 | 239 | t.Run("desc", func(t *testing.T) { 240 | t.Parallel() 241 | 242 | r := pipe.Range(10_000, 0, -5).Do() 243 | idx := 0 244 | for i := 10_000; i > 0; i -= 5 { 245 | require.Equal(t, i, r[idx]) 246 | idx++ 247 | } 248 | }) 249 | 250 | t.Run("desc2", func(t *testing.T) { 251 | t.Parallel() 252 | 253 | r := pipe.Range(0, -10_000, -5).Do() 254 | idx := 0 255 | for i := 0; i > -10_000; i -= 5 { 256 | require.Equal(t, i, r[idx]) 257 | idx++ 258 | } 259 | }) 260 | } 261 | 262 | func TestRepeat(t *testing.T) { 263 | t.Parallel() 264 | 265 | const r = 1 266 | 267 | exp := [][]int{{}, {r, r, r, r, r}, {}} 268 | for i, n := range []int{0, 5, -1} { 269 | require.Equal(t, exp[i], pipe.Repeat(r, n).Do()) 270 | } 271 | } 272 | 273 | // testing pipe and pipeNL functions 274 | 275 | func TestMap(t *testing.T) { 276 | t.Parallel() 277 | 278 | cases := []struct { 279 | name string 280 | input pipe.Piper[int] 281 | inputNL pipe.PiperNoLen[int] 282 | f func(int) int 283 | want []int 284 | }{ 285 | { 286 | name: "double", 287 | input: pipe.Slice([]int{1, 2, 3}), 288 | f: func(i int) int { return i * 2 }, 289 | want: []int{2, 4, 6}, 290 | }, 291 | { 292 | name: "empty", 293 | input: pipe.Slice([]int{}), 294 | f: func(i int) int { return i }, 295 | want: []int{}, 296 | }, 297 | { 298 | name: "single_element", 299 | input: pipe.Slice([]int{1}), 300 | f: func(i int) int { return i }, 301 | want: []int{1}, 302 | }, 303 | { 304 | name: "many_diffeerent_elements", 305 | input: pipe.Slice(largeSlice()), 306 | f: func(x int) int { return x * 2 }, 307 | want: func() []int { 308 | b := make([]int, len(largeSlice())) 309 | ls := largeSlice() 310 | for i := range ls { 311 | b[i] = ls[i] * 2 312 | } 313 | return b 314 | }(), 315 | }, 316 | { 317 | name: "many_diffeerent_elements_fn", 318 | inputNL: pipe.Func(func(i int) (int, bool) { 319 | return largeSlice()[i], i < len(largeSlice()) 320 | }), 321 | f: func(x int) int { return x * 2 }, 322 | want: func() []int { 323 | b := make([]int, len(largeSlice())) 324 | ls := largeSlice() 325 | for i := range ls { 326 | b[i] = ls[i] * 2 327 | } 328 | return b 329 | }(), 330 | }, 331 | } 332 | 333 | for _, c := range cases { 334 | c := c 335 | 336 | t.Run(c.name, func(t *testing.T) { 337 | t.Parallel() 338 | 339 | if c.inputNL != nil { 340 | res := c.inputNL.Map(c.f).Gen(len(largeSlice())).Do() 341 | require.Equal(t, c.want, res) 342 | } else { 343 | res := c.input.Map(c.f).Do() 344 | require.Equal(t, c.want, res) 345 | } 346 | }) 347 | t.Run(c.name+"_parallel", func(t *testing.T) { 348 | t.Parallel() 349 | 350 | if c.inputNL != nil { 351 | res := c.inputNL.Map(c.f).Gen(len(largeSlice())).Parallel(7).Do() 352 | require.Equal(t, c.want, res) 353 | } else { 354 | res := c.input.Map(c.f).Parallel(7).Do() 355 | require.Equal(t, c.want, res) 356 | } 357 | }) 358 | } 359 | } 360 | 361 | // FIXME: this test is ok but ugly, need to be refactored 362 | func TestFilter(t *testing.T) { 363 | t.Parallel() 364 | 365 | genFunc := func(i int) (*float64, bool) { 366 | if i%10 == 0 { 367 | return nil, true 368 | } 369 | return pointer.Ref(float64(i)), true 370 | } 371 | 372 | s := pipe.MapNL( 373 | pipe.Func(genFunc). 374 | Filter(pipies.NotNil[*float64]), 375 | pointer.Deref[float64], 376 | ).Take(10_000).Sum(pipies.Sum[float64]) 377 | require.NotNil(t, s) 378 | 379 | sm := 0 380 | a := make([]*float64, 0) 381 | for i := 0; i < 10000; i++ { 382 | f := float64(i) 383 | a = append(a, &f) 384 | if i%10 != 0 { 385 | sm += i 386 | } 387 | } 388 | require.Equal(t, float64(sm), s) 389 | 390 | ss := pipe.Map( 391 | pipe.Slice(a).Map(func(x *float64) *float64 { 392 | if int(*x)%10 != 0 { 393 | return x 394 | } 395 | return nil 396 | }).Filter(pipies.NotNil[*float64]), 397 | pointer.Deref[float64], 398 | ).Sum(pipies.Sum[float64]) 399 | require.Equal(t, float64(sm), ss) 400 | } 401 | 402 | func TestMapFilter(t *testing.T) { 403 | t.Parallel() 404 | 405 | cases := []struct { 406 | name string 407 | source []int 408 | gen int 409 | funcSource func(int) (int, bool) 410 | apply func(pipe.Piper[int]) pipe.Piper[int] 411 | applyNL func(pipe.PiperNoLen[int]) pipe.PiperNoLen[int] 412 | expect []int 413 | }{ 414 | { 415 | name: "simple", 416 | source: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 417 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 418 | return p.MapFilter(func(x int) (int, bool) { 419 | return x * 3, x%2 == 0 420 | }) 421 | }, 422 | expect: []int{6, 12, 18, 24, 0}, 423 | }, 424 | { 425 | name: "simple_fn", 426 | funcSource: func(i int) (int, bool) { 427 | if i < 10 { 428 | return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}[i], true 429 | } 430 | return 0, true 431 | }, 432 | gen: 10, 433 | applyNL: func(p pipe.PiperNoLen[int]) pipe.PiperNoLen[int] { 434 | return p.MapFilter(func(x int) (int, bool) { 435 | return x * 3, x%2 == 0 436 | }) 437 | }, 438 | expect: []int{6, 12, 18, 24, 0}, 439 | }, 440 | } 441 | 442 | for _, c := range cases { 443 | c := c 444 | t.Run(c.name, func(t *testing.T) { 445 | t.Parallel() 446 | 447 | var p pipe.Piper[int] 448 | if c.funcSource != nil { 449 | p = c.applyNL(pipe.Func(c.funcSource)).Gen(c.gen) 450 | } else { 451 | p = c.apply(pipe.Slice(c.source)) 452 | } 453 | 454 | res := p.Do() 455 | require.Equal(t, c.expect, res) 456 | }) 457 | t.Run(c.name+"_parallel", func(t *testing.T) { 458 | t.Parallel() 459 | 460 | var p pipe.Piper[int] 461 | if c.funcSource != nil { 462 | p = c.applyNL(pipe.Func(c.funcSource)).Parallel(7).Gen(c.gen) 463 | } else { 464 | p = c.apply(pipe.Slice(c.source)).Parallel(7) 465 | } 466 | 467 | res := p.Do() 468 | require.Equal(t, c.expect, res) 469 | }) 470 | } 471 | } 472 | 473 | func TestSort(t *testing.T) { 474 | t.Parallel() 475 | 476 | testCases := []struct { 477 | name string 478 | size int 479 | parallel uint16 480 | }{ 481 | { 482 | name: "Simple", 483 | size: 50_000, 484 | parallel: 4, 485 | }, 486 | { 487 | name: "Single_thread", 488 | size: 400_000, 489 | parallel: 1, 490 | }, 491 | { 492 | name: "Single_thread_tiny", 493 | size: 3, 494 | parallel: 1, 495 | }, 496 | { 497 | name: "Single_thread_empty", 498 | size: 0, 499 | parallel: 1, 500 | }, 501 | { 502 | name: "MultiThread", 503 | size: 400_000, 504 | parallel: 8, 505 | }, 506 | { 507 | name: "tiny", 508 | size: 3, 509 | parallel: 8, 510 | }, 511 | { 512 | name: "one", 513 | size: 1, 514 | parallel: 8, 515 | }, 516 | { 517 | name: "empty", 518 | size: 0, 519 | parallel: 8, 520 | }, 521 | { 522 | name: "smallArray", 523 | size: 6000, 524 | parallel: 8, 525 | }, 526 | { 527 | name: "Many_threads", 528 | size: 6000, 529 | parallel: 8000, 530 | }, 531 | } 532 | 533 | for _, tc := range testCases { 534 | tc := tc 535 | t.Run(tc.name, func(t *testing.T) { 536 | t.Parallel() 537 | 538 | a, err := readTestData() 539 | require.Nil(t, err) 540 | a = a[:tc.size] 541 | res := pipe.Slice(a).Sort(pipies.Less[int]).Parallel(tc.parallel).Do() 542 | for i := 0; i < len(res)-1; i++ { 543 | require.LessOrEqual(t, res[i], res[i+1]) 544 | } 545 | }) 546 | } 547 | } 548 | 549 | func TestReduce(t *testing.T) { 550 | t.Parallel() 551 | 552 | res := pipe.Func(func(i int) (int, bool) { 553 | return i, true 554 | }). 555 | Gen(6000). 556 | Reduce(func(a, b *int) int { return *a + *b }) 557 | 558 | expected := 0 559 | a := make([]int, 0) 560 | for i := 0; i < 6000; i++ { 561 | expected += i 562 | a = append(a, i) 563 | } 564 | require.Equal(t, expected, *res) 565 | 566 | res = pipe.Slice(a).Reduce(func(a, b *int) int { return *a + *b }) 567 | require.Equal(t, expected, *res) 568 | } 569 | 570 | func TestSum(t *testing.T) { 571 | t.Parallel() 572 | 573 | res := pipe.Func(func(i int) (int, bool) { 574 | return i, true 575 | }). 576 | Gen(6000). 577 | Parallel(6000). 578 | Sum(func(a, b *int) int { return *a + *b }) 579 | 580 | expected := 0 581 | a := make([]int, 0) 582 | for i := 0; i < 6000; i++ { 583 | expected += i 584 | a = append(a, i) 585 | } 586 | 587 | require.Equal(t, expected, res) 588 | 589 | res = pipe.Slice(a).Sum(func(a, b *int) int { return *a + *b }) 590 | require.Equal(t, expected, res) 591 | } 592 | 593 | func TestFirst(t *testing.T) { 594 | t.Parallel() 595 | 596 | const limit = 100_000 597 | 598 | testCases := []struct { 599 | name string 600 | function func() *float64 601 | expectedFirst float64 602 | }{ 603 | { 604 | name: "Slice_First_Filtered", 605 | function: func() *float64 { 606 | return ff.Map(largeSlice(), func(x int) float64 { 607 | return float64(x) 608 | }). 609 | Filter(func(x *float64) bool { return *x > limit }). 610 | First() 611 | }, 612 | expectedFirst: float64(100489), 613 | }, 614 | { 615 | name: "Func_First_Limited", 616 | function: func() *float64 { 617 | return pipe.Func(func(i int) (float64, bool) { return float64(i), true }). 618 | Filter(func(x *float64) bool { return *x > 10_000 }). 619 | Take(limit). 620 | First() 621 | }, 622 | expectedFirst: 10001, 623 | }, 624 | { 625 | name: "Func_First_No_Limit", 626 | function: func() *float64 { 627 | return pipe.Fn(func(i int) float64 { return float64(i) }). 628 | Filter(func(x *float64) bool { return *x > limit }). 629 | First() 630 | }, 631 | expectedFirst: float64(limit) + 1.0, 632 | }, 633 | } 634 | 635 | for _, tc := range testCases { 636 | tc := tc 637 | t.Run(tc.name, func(t *testing.T) { 638 | t.Parallel() 639 | 640 | s := tc.function() 641 | 642 | require.NotNil(t, s) 643 | require.Equal(t, tc.expectedFirst, *s) 644 | }) 645 | } 646 | } 647 | 648 | func TestAny(t *testing.T) { 649 | t.Parallel() 650 | 651 | cases := []struct { 652 | name string 653 | source []int 654 | take int 655 | funcSource func(int) (int, bool) 656 | apply func(pipe.Piper[int]) pipe.Piper[int] 657 | applyNL func(pipe.PiperNoLen[int]) pipe.PiperNoLen[int] 658 | expect []int 659 | }{ 660 | { 661 | name: "simple", 662 | source: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 663 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 664 | return p 665 | }, 666 | expect: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 667 | }, 668 | { 669 | name: "simple_fn", 670 | funcSource: func(i int) (int, bool) { 671 | if i < 10 { 672 | return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}[i], true 673 | } 674 | return 0, false 675 | }, 676 | take: 10, 677 | applyNL: func(p pipe.PiperNoLen[int]) pipe.PiperNoLen[int] { 678 | return p 679 | }, 680 | expect: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 681 | }, 682 | } 683 | 684 | for _, c := range cases { 685 | c := c 686 | t.Run(c.name, func(t *testing.T) { 687 | t.Parallel() 688 | 689 | var p pipe.Piper[int] 690 | if c.funcSource != nil { 691 | rs := c.applyNL(pipe.Func(c.funcSource)).Any() 692 | require.Contains(t, c.expect, *rs) 693 | return 694 | } 695 | 696 | p = c.apply(pipe.Slice(c.source)) 697 | res := p.Any() 698 | require.Contains(t, c.expect, *res) 699 | }) 700 | t.Run(c.name+"_parallel", func(t *testing.T) { 701 | t.Parallel() 702 | 703 | var p pipe.Piper[int] 704 | if c.funcSource != nil { 705 | rs := c.applyNL(pipe.Func(c.funcSource)).Parallel(7).Any() 706 | require.Contains(t, c.expect, *rs) 707 | return 708 | } 709 | 710 | p = c.apply(pipe.Slice(c.source)) 711 | res := p.Parallel(7).Any() 712 | require.Contains(t, c.expect, *res) 713 | }) 714 | } 715 | } 716 | 717 | func TestCount(t *testing.T) { 718 | t.Parallel() 719 | 720 | cases := []struct { 721 | name string 722 | source []int 723 | take int 724 | funcSource func(int) (int, bool) 725 | apply func(pipe.Piper[int]) pipe.Piper[int] 726 | expect int 727 | }{ 728 | { 729 | name: "simple", 730 | source: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 731 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 732 | return p 733 | }, 734 | expect: 10, 735 | }, 736 | { 737 | name: "zero", 738 | source: []int{}, 739 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 740 | return p 741 | }, 742 | expect: 0, 743 | }, 744 | { 745 | name: "simple_fn", 746 | funcSource: func(i int) (int, bool) { return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}[i], true }, 747 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 748 | return p 749 | }, 750 | take: 10, 751 | expect: 10, 752 | }, 753 | { 754 | name: "zero_fn", 755 | funcSource: func(i int) (int, bool) { return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}[i], true }, 756 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 757 | return p 758 | }, 759 | take: 0, 760 | expect: 0, 761 | }, 762 | } 763 | 764 | for _, c := range cases { 765 | c := c 766 | 767 | var p pipe.Piper[int] 768 | if c.funcSource != nil { 769 | p = c.apply(pipe.Func(c.funcSource).Take(c.take)) 770 | } else { 771 | p = c.apply(pipe.Slice(c.source)) 772 | } 773 | 774 | t.Run(c.name, func(t *testing.T) { 775 | t.Parallel() 776 | res := p.Count() 777 | require.Equal(t, c.expect, res) 778 | }) 779 | t.Run(c.name+"_parallel", func(t *testing.T) { 780 | t.Parallel() 781 | res := p.Parallel(7).Count() 782 | require.Equal(t, c.expect, res) 783 | }) 784 | } 785 | } 786 | 787 | func TestPromices(t *testing.T) { 788 | t.Parallel() 789 | 790 | cases := []struct { 791 | name string 792 | source []int 793 | take int 794 | funcSource func(int) (int, bool) 795 | apply func(pipe.Piper[int]) pipe.Piper[int] 796 | expect []int 797 | }{ 798 | { 799 | name: "simple", 800 | source: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 801 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 802 | return p 803 | }, 804 | expect: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 805 | }, 806 | { 807 | name: "zero", 808 | source: []int{}, 809 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 810 | return p 811 | }, 812 | expect: []int{}, 813 | }, 814 | { 815 | name: "simple_fn", 816 | funcSource: func(i int) (int, bool) { return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}[i], true }, 817 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 818 | return p 819 | }, 820 | take: 10, 821 | expect: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 822 | }, 823 | } 824 | 825 | for _, c := range cases { 826 | c := c 827 | 828 | var p pipe.Piper[int] 829 | if c.funcSource != nil { 830 | p = c.apply(pipe.Func(c.funcSource).Take(c.take)) 831 | } else { 832 | p = c.apply(pipe.Slice(c.source)) 833 | } 834 | 835 | t.Run(c.name, func(t *testing.T) { 836 | t.Parallel() 837 | 838 | res := p.Promices() 839 | resAr := make([]int, len(res)) 840 | for i := range res { 841 | x, _ := res[i]() 842 | resAr[i] = x 843 | } 844 | require.Equal(t, c.expect, resAr) 845 | }) 846 | t.Run(c.name+"_parallel", func(t *testing.T) { 847 | t.Parallel() 848 | 849 | res := p.Parallel(7).Promices() 850 | resAr := make([]int, len(res)) 851 | for i := range res { 852 | x, _ := res[i]() 853 | resAr[i] = x 854 | } 855 | require.Equal(t, c.expect, resAr) 856 | }) 857 | } 858 | } 859 | 860 | func TestErase(t *testing.T) { 861 | t.Parallel() 862 | 863 | cases := []struct { 864 | name string 865 | source []int 866 | take int 867 | funcSource func(int) (int, bool) 868 | apply func(pipe.Piper[any]) pipe.Piper[any] 869 | expect []int 870 | }{ 871 | { 872 | name: "simple", 873 | source: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 874 | apply: func(p pipe.Piper[any]) pipe.Piper[any] { 875 | return p 876 | }, 877 | expect: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 878 | }, 879 | { 880 | name: "zero", 881 | source: []int{}, 882 | apply: func(p pipe.Piper[any]) pipe.Piper[any] { 883 | return p 884 | }, 885 | expect: []int{}, 886 | }, 887 | { 888 | name: "simple_fn", 889 | funcSource: func(i int) (int, bool) { return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}[i], true }, 890 | take: 10, 891 | apply: func(p pipe.Piper[any]) pipe.Piper[any] { 892 | return p 893 | }, 894 | expect: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 895 | }, 896 | } 897 | 898 | for _, c := range cases { 899 | c := c 900 | 901 | var p pipe.Piper[any] 902 | if c.funcSource != nil { 903 | p = c.apply(pipe.Func(c.funcSource).Erase().Take(c.take)) 904 | } else { 905 | p = c.apply(pipe.Slice(c.source).Erase()) 906 | } 907 | 908 | t.Run(c.name, func(t *testing.T) { 909 | t.Parallel() 910 | 911 | res := p.Do() 912 | resAr := make([]int, len(res)) 913 | for i := range res { 914 | resAr[i] = *(res[i].(*int)) 915 | } 916 | require.Equal(t, c.expect, resAr) 917 | }) 918 | t.Run(c.name+"_parallel", func(t *testing.T) { 919 | t.Parallel() 920 | 921 | res := p.Parallel(7).Do() 922 | resAr := make([]int, len(res)) 923 | for i := range res { 924 | resAr[i] = *(res[i].(*int)) 925 | } 926 | require.Equal(t, c.expect, resAr) 927 | }) 928 | } 929 | } 930 | 931 | func TestYetiSnag(t *testing.T) { 932 | t.Parallel() 933 | 934 | randErr := errors.New("test err") 935 | simpleTestErr := errors.New("simple") 936 | simpleFnTestErr := errors.New("simpleFn") 937 | 938 | mx := sync.Mutex{} 939 | sharedCnt := 0 940 | shared := make(map[int]any) 941 | 942 | cases := []struct { 943 | name string 944 | source []int 945 | take int 946 | funcSource func(int) (int, bool) 947 | apply func(pipe.Piper[int]) pipe.Piper[int] 948 | applyNL func(pipe.PiperNoLen[int]) pipe.PiperNoLen[int] 949 | expect func(*testing.T) 950 | }{ 951 | { 952 | name: "simple", 953 | source: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 954 | apply: func(p pipe.Piper[int]) pipe.Piper[int] { 955 | y := pipe.NewYeti() 956 | return p. 957 | Yeti(y). 958 | Map(func(i int) int { 959 | if i == 5 { 960 | y.Yeet(errors.Join(simpleTestErr, randErr)) 961 | } 962 | return i 963 | }). 964 | Snag(func(e error) { 965 | mx.Lock() 966 | sharedCnt++ 967 | shared[sharedCnt] = e 968 | mx.Unlock() 969 | }) 970 | }, 971 | expect: func(t *testing.T) { 972 | mx.Lock() 973 | defer mx.Unlock() 974 | found := false 975 | for _, v := range shared { 976 | er := v.(error) 977 | if errors.Is(er, simpleTestErr) { 978 | found = true 979 | break 980 | } 981 | } 982 | require.True(t, found, "simple test case error was not found") 983 | }, 984 | }, 985 | { 986 | name: "simple_fn", 987 | funcSource: func(i int) (int, bool) { return []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}[i], true }, 988 | take: 10, 989 | applyNL: func(p pipe.PiperNoLen[int]) pipe.PiperNoLen[int] { 990 | y := pipe.NewYeti() 991 | return p. 992 | Yeti(y). 993 | Map(func(i int) int { 994 | if i == 5 { 995 | y.Yeet(errors.Join(simpleFnTestErr, randErr)) 996 | } 997 | return i 998 | }). 999 | Snag(func(e error) { 1000 | mx.Lock() 1001 | sharedCnt++ 1002 | shared[sharedCnt] = e 1003 | mx.Unlock() 1004 | }) 1005 | }, 1006 | expect: func(t *testing.T) { 1007 | mx.Lock() 1008 | defer mx.Unlock() 1009 | found := false 1010 | for _, v := range shared { 1011 | er := v.(error) 1012 | if errors.Is(er, simpleFnTestErr) { 1013 | found = true 1014 | break 1015 | } 1016 | } 1017 | require.True(t, found, "simple test case error was not found") 1018 | }, 1019 | }, 1020 | } 1021 | 1022 | for _, c := range cases { 1023 | c := c 1024 | 1025 | var p pipe.Piper[int] 1026 | if c.funcSource != nil { 1027 | p = c.applyNL(pipe.Func(c.funcSource)).Take(c.take) 1028 | } else { 1029 | p = c.apply(pipe.Slice(c.source)) 1030 | } 1031 | 1032 | t.Run(c.name, func(t *testing.T) { 1033 | t.Parallel() 1034 | 1035 | _ = p.Do() 1036 | c.expect(t) 1037 | }) 1038 | t.Run(c.name+"_parallel", func(t *testing.T) { 1039 | t.Parallel() 1040 | 1041 | _ = p.Parallel(7).Do() 1042 | c.expect(t) 1043 | }) 1044 | } 1045 | } 1046 | 1047 | // prefixpipe 1048 | 1049 | func TestPrefixMap(t *testing.T) { 1050 | res := pipe.Map( 1051 | pipe.Slice([]int{1, 2, 3, 4}). 1052 | Filter(func(x *int) bool { 1053 | return *x != 2 1054 | }), 1055 | func(x int) string { 1056 | return strconv.Itoa(x) 1057 | }, 1058 | ).Do() 1059 | require.Equal(t, []string{"1", "3", "4"}, res) 1060 | } 1061 | 1062 | func TestPrefixMapNL(t *testing.T) { 1063 | t.Parallel() 1064 | 1065 | res := pipe.MapNL( 1066 | pipe.Func(func(i int) (int, bool) { 1067 | return []int{1, 2, 3, 4}[i], true 1068 | }).Filter(func(x *int) bool { 1069 | return *x != 2 1070 | }), 1071 | func(x int) string { 1072 | return strconv.Itoa(x) 1073 | }, 1074 | ).Take(3).Do() 1075 | require.Equal(t, []string{"1", "3", "4"}, res) 1076 | } 1077 | 1078 | func TestPrefixMapFilter(t *testing.T) { 1079 | res := pipe.MapFilter( 1080 | pipe.Slice([]int{1, 2, 3, 4, 5}).Filter( 1081 | func(x *int) bool { return *x != 5 }, 1082 | ), 1083 | func(x int) (string, bool) { 1084 | return strconv.Itoa(x), x != 2 1085 | }, 1086 | ).Do() 1087 | require.Equal(t, []string{"1", "3", "4"}, res) 1088 | } 1089 | 1090 | func TestPrefixMapFilterNL(t *testing.T) { 1091 | t.Parallel() 1092 | 1093 | res := pipe.MapFilterNL( 1094 | pipe.Func(func(i int) (int, bool) { 1095 | a := [...]int{1, 2, 3, 4, 5} 1096 | return a[i], a[i] != 5 1097 | }), 1098 | func(x int) (string, bool) { 1099 | return strconv.Itoa(x), x != 2 1100 | }, 1101 | ).Gen(5).Do() 1102 | require.Equal(t, []string{"1", "3", "4"}, res) 1103 | } 1104 | 1105 | func TestPrefixReduce(t *testing.T) { 1106 | t.Parallel() 1107 | 1108 | t.Run("common", func(t *testing.T) { 1109 | t.Parallel() 1110 | res := pipe.Reduce( 1111 | pipe.Slice([]int{1, 2, 3, 4, 5}), 1112 | func(s *string, n *int) string { 1113 | return *s + strconv.Itoa(*n) 1114 | }, 1115 | "the result string is: ", 1116 | ) 1117 | require.Equal(t, "the result string is: 12345", res) 1118 | }) 1119 | 1120 | t.Run("zero_res", func(t *testing.T) { 1121 | t.Parallel() 1122 | res := pipe.Reduce( 1123 | pipe.Slice([]int{1, 2, 3, 4, 5}). 1124 | Filter(func(x *int) bool { return *x > 100 }), 1125 | func(s *string, n *int) string { 1126 | return *s + strconv.Itoa(*n) 1127 | }, 1128 | "the result string is: ", 1129 | ) 1130 | require.Equal(t, "the result string is: ", res) 1131 | }) 1132 | 1133 | t.Run("single_res", func(t *testing.T) { 1134 | t.Parallel() 1135 | res := pipe.Reduce( 1136 | pipe.Slice([]int{1, 2, 3, 4, 5}). 1137 | Filter(func(x *int) bool { return *x == 5 }), 1138 | func(s *string, n *int) string { 1139 | return *s + strconv.Itoa(*n) 1140 | }, 1141 | "the result string is: ", 1142 | ) 1143 | require.Equal(t, "the result string is: 5", res) 1144 | }) 1145 | } 1146 | 1147 | // functype test 1148 | 1149 | func TestAcc(t *testing.T) { 1150 | t.Parallel() 1151 | 1152 | res := pipe.Slice([]int{1, 2, 3, 4, 5}).Reduce( 1153 | pipe.Acc(func(x int, y int) int { 1154 | return x + y 1155 | }), 1156 | ) 1157 | require.Equal(t, 15, *res) 1158 | } 1159 | 1160 | func TestPred(t *testing.T) { 1161 | t.Parallel() 1162 | 1163 | res := pipe.Slice([]int{1, 2, 3, 4, 5}).Filter( 1164 | pipe.Pred(func(x int) bool { 1165 | return x != 2 1166 | }), 1167 | ).Do() 1168 | require.Equal(t, []int{1, 3, 4, 5}, res) 1169 | } 1170 | 1171 | func TestComp(t *testing.T) { 1172 | t.Parallel() 1173 | 1174 | res := pipe.Slice([]int{5, 3, 4, 1, 3, 5, 2, 3, 6}).Sort( 1175 | pipe.Comp(func(x, y int) bool { return x < y }), 1176 | ).Do() 1177 | require.Equal(t, []int{1, 2, 3, 3, 3, 4, 5, 5, 6}, res) 1178 | } 1179 | 1180 | // helping functions 1181 | 1182 | func wrap[T any](x T) func() T { 1183 | return func() T { 1184 | return x 1185 | } 1186 | } 1187 | 1188 | var ( 1189 | a []int 1190 | mx1 sync.Mutex 1191 | 1192 | pls pipe.Piper[int] 1193 | mx2 sync.Mutex 1194 | 1195 | testSlice []int 1196 | mxTestSlice sync.Mutex 1197 | ) 1198 | 1199 | func largeSlice() []int { 1200 | const largeSize = 400_000 1201 | mx1.Lock() 1202 | defer mx1.Unlock() 1203 | 1204 | if a == nil { 1205 | a = make([]int, largeSize) 1206 | for i := range a { 1207 | a[i] = i * i 1208 | } 1209 | } 1210 | return a 1211 | } 1212 | 1213 | func pipeLargeSlice() pipe.Piper[int] { 1214 | mx2.Lock() 1215 | defer mx2.Unlock() 1216 | 1217 | if pls == nil { 1218 | pls = pipe.Slice(largeSlice()) 1219 | } 1220 | 1221 | return pls 1222 | } 1223 | 1224 | func readTestData() ([]int, error) { 1225 | mxTestSlice.Lock() 1226 | defer mxTestSlice.Unlock() 1227 | if len(testSlice) != 0 { 1228 | return testSlice, nil 1229 | } 1230 | raw, err := os.ReadFile("../../.test_data/test1.txt") 1231 | if err != nil { 1232 | return nil, err 1233 | } 1234 | a := make([]int, 0) 1235 | for _, val := range strings.Split(string(raw), " ") { 1236 | val = strings.Trim(val, "][,") 1237 | ival, err := strconv.Atoi(val) 1238 | if err != nil { 1239 | return nil, err 1240 | } 1241 | a = append(a, ival) 1242 | } 1243 | 1244 | testSlice = a 1245 | return a, nil 1246 | } 1247 | --------------------------------------------------------------------------------