diff --git a/gofp/any.go b/gofp/any.go deleted file mode 100644 index 42f2845..0000000 --- a/gofp/any.go +++ /dev/null @@ -1,12 +0,0 @@ -package gofp - -// Any returns if exist at least one element that satisfy the predicate. -func Any[T any](list []T, predicate func(T) bool) (res bool) { - for i := range list { - if predicate(list[i]) { - return true - } - } - - return false -} diff --git a/gofp/contains.go b/gofp/contains.go new file mode 100644 index 0000000..4eace0e --- /dev/null +++ b/gofp/contains.go @@ -0,0 +1,59 @@ +package gofp + +// ContainsAll checks if all elements from needles list are in the haystack. +func ContainsAll[T comparable](haystack []T, needles ...T) bool { + // Avoid allocations for a single check. + if len(needles) == 1 { + for _, h := range haystack { + if h == needles[0] { + return true + } + } + return false + } + + checks := make(map[T]struct{}, len(needles)) + for _, n := range needles { + checks[n] = struct{}{} + } + + for _, h := range haystack { + _, ok := checks[h] + if ok { + delete(checks, h) + + if len(checks) == 0 { + return true + } + } + } + + return false +} + +// ContainsAtLeastOne checks if at least one element from needles list is in the haystack. +func ContainsAtLeastOne[T comparable](haystack []T, needles ...T) bool { + // Avoid allocations for a single check. + if len(needles) == 1 { + for _, h := range haystack { + if h == needles[0] { + return true + } + } + return false + } + + checks := make(map[T]struct{}, len(needles)) + for _, n := range needles { + checks[n] = struct{}{} + } + + for _, h := range haystack { + _, ok := checks[h] + if ok { + return true + } + } + + return false +} diff --git a/gofp/contains_test.go b/gofp/contains_test.go new file mode 100644 index 0000000..734c4cc --- /dev/null +++ b/gofp/contains_test.go @@ -0,0 +1,157 @@ +package gofp_test + +import ( + "testing" + + . "github.com/msales/gox/gofp" +) + +func TestContainsAll(t *testing.T) { + tests := []struct { + name string + slice []int64 + contains []int64 + want bool + }{ + { + name: "found 1 element", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{3}, + want: true, + }, + { + name: "found 3 elements", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{3, 5, 6}, + want: true, + }, + { + name: "found some but not all elements", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{3, 5, 6, 7}, + want: false, + }, + { + name: "found no elements", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{7, 9}, + want: false, + }, + { + name: "found no element", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{7}, + want: false, + }, + { + name: "found all in equal slices", + slice: []int64{1, 2, 3}, + contains: []int64{1, 2, 3}, + want: true, + }, + { + name: "found some with needles containing all slice elements plus some", + slice: []int64{1, 2, 3}, + contains: []int64{1, 2, 3, 4}, + want: false, + }, + { + name: "found no element in empty slicee", + slice: []int64{}, + contains: []int64{1, 2, 3, 4}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ContainsAll(tt.slice, tt.contains...) + if tt.want != got { + t.Errorf("Got %+v, want %+v", got, tt.want) + } + }) + } +} + +func BenchmarkContainsAll_Single(b *testing.B) { + haystack := []int64{1, 2, 3, 4, 5, 6} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + ContainsAll(haystack, 3) + } +} + +func BenchmarkContainsAll_Multiple(b *testing.B) { + haystack := []int64{1, 2, 3, 4, 5, 6} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + ContainsAll(haystack, 2, 3) + } +} + +func TestContainsAtLeastOne(t *testing.T) { + tests := []struct { + name string + slice []int64 + contains []int64 + want bool + }{ + { + name: "found 1 element", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{3}, + want: true, + }, + { + name: "found 3 elements", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{3, 5, 6}, + want: true, + }, + { + name: "found some but not all elements", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{3, 5, 6, 7}, + want: true, + }, + { + name: "found no elements", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{7, 9}, + want: false, + }, + { + name: "found no element", + slice: []int64{1, 2, 3, 4, 5, 6}, + contains: []int64{7}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ContainsAtLeastOne(tt.slice, tt.contains...) + if tt.want != got { + t.Errorf("Got %+v, want %+v", got, tt.want) + } + }) + } +} + +func BenchmarkContainsAtLeastOne_Single(b *testing.B) { + haystack := []int64{1, 2, 3, 4, 5, 6} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + ContainsAtLeastOne(haystack, 3) + } +} + +func BenchmarkContainsAtLeastOne_Multiple(b *testing.B) { + haystack := []int64{1, 2, 3, 4, 5, 6} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + ContainsAtLeastOne(haystack, 2, 3) + } +} diff --git a/gofp/diff.go b/gofp/diff.go new file mode 100644 index 0000000..ef59a3b --- /dev/null +++ b/gofp/diff.go @@ -0,0 +1,52 @@ +package gofp + +// SymDiff returns symmetric difference of 2 slices. +// https://en.wikipedia.org/wiki/Symmetric_difference +func SymDiff[T comparable](slice1, slice2 []T) []T { + checks := make(map[T]int8, len(slice1)+len(slice2)) + idx := make([]T, 0, len(slice1)+len(slice2)) + for _, v := range slice1 { + checks[v]++ + idx = append(idx, v) + } + for _, v := range slice2 { + checks[v]++ + if checks[v] == 1 { + idx = append(idx, v) + } + } + + outer := make([]T, 0, len(slice1)+len(slice2)) + for _, id := range idx { + if checks[id] == 1 { + outer = append(outer, id) + } + } + + return outer +} + +// DiffLeft returns left diff of 2 slices. +func DiffLeft[T comparable](slice1, slice2 []T) []T { + checks := make(map[T]int8, len(slice1)) + for _, v := range slice1 { + checks[v]++ + } + for _, v := range slice2 { + checks[v]++ + } + + outer := make([]T, 0, len(slice1)) + for _, id := range slice1 { + if checks[id] == 1 { + outer = append(outer, id) + } + } + + return outer +} + +// DiffRight returns right diff of 2 slices. +func DiffRight[T comparable](slice1, slice2 []T) []T { + return DiffLeft(slice2, slice1) +} diff --git a/gofp/diff_test.go b/gofp/diff_test.go new file mode 100644 index 0000000..92399ca --- /dev/null +++ b/gofp/diff_test.go @@ -0,0 +1,308 @@ +package gofp + +import ( + "math/rand" + "testing" +) + +func TestSymDiff(t *testing.T) { + tests := []struct { + name string + slice1 []int + slice2 []int + want []int + }{ + { + name: "diff left slice", + slice1: []int{1, 2, 3}, + slice2: []int{3}, + want: []int{1, 2}, + }, + { + name: "diff right slice", + slice1: []int{3}, + slice2: []int{1, 2, 3}, + want: []int{1, 2}, + }, + { + name: "diff both slices", + slice1: []int{3, 4, 5}, + slice2: []int{1, 2, 3}, + want: []int{4, 5, 1, 2}, + }, + { + name: "diff both slices - same values", + slice1: []int{1, 2, 3, 4, 5, 6}, + slice2: []int{1, 2, 3, 4, 5, 6}, + want: []int{}, + }, + { + name: "diff empty", + slice1: []int{}, + slice2: []int{}, + want: []int{}, + }, + { + name: "diff both - pessimistic", + slice1: []int{1, 2, 3, 4, 5, 6}, + slice2: []int{7, 8, 9, 10, 11, 12}, + want: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SymDiff(tt.slice1, tt.slice2) + if tt.want == nil && got != nil { + t.Errorf("Expected nil result got %+v instead", got) + return + } + + if len(tt.want) != len(got) { + t.Errorf("Got %+v, want %+v", got, tt.want) + return + } + + for k, v := range tt.want { + if v != got[k] { + t.Errorf("Got %+v, want %+v", got, tt.want) + } + } + }) + } +} + +func BenchmarkSymDiff_Both(b *testing.B) { + slice1 := []int{1, 2, 3, 4, 5, 6} + slice2 := []int{5, 6, 7, 8, 9, 10} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + SymDiff(slice1, slice2) + } +} + +func BenchmarkSymDiff_Both_Pessimistic(b *testing.B) { + slice1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + slice2 := []int{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + SymDiff(slice1, slice2) + } +} + +func BenchmarkSymDiff_Both_MoreValues_Random(b *testing.B) { + var slice1, slice2 []int + for i := 0; i < 300; i++ { + slice1 = append(slice1, i) + } + + for i := 0; i < 50; i++ { + slice2 = append(slice2, i) + } + rand.Shuffle(len(slice1), func(i, j int) { slice1[i], slice1[j] = slice1[j], slice1[i] }) + rand.Shuffle(len(slice2), func(i, j int) { slice2[i], slice2[j] = slice2[j], slice2[i] }) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + SymDiff(slice1, slice2) + } +} + +func BenchmarkSymDiff_Both_Pessimistic_RightShorter(b *testing.B) { + slice1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + slice2 := []int{17, 18, 19, 20} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + SymDiff(slice1, slice2) + } +} + +func TestDiffLeft(t *testing.T) { + tests := []struct { + name string + slice1 []int + slice2 []int + want []int + }{ + { + name: "diff left slice", + slice1: []int{1, 2, 3}, + slice2: []int{3}, + want: []int{1, 2}, + }, + { + name: "diff right slice", + slice1: []int{3}, + slice2: []int{1, 2, 3}, + want: []int{}, + }, + { + name: "diff both", + slice1: []int{3, 4, 5}, + slice2: []int{1, 2, 3}, + want: []int{4, 5}, + }, + { + name: "diff both slices - same values", + slice1: []int{1, 2, 3, 4, 5, 6}, + slice2: []int{1, 2, 3, 4, 5, 6}, + want: []int{}, + }, + { + name: "diff empty", + slice1: []int{}, + slice2: []int{}, + want: []int{}, + }, + { + name: "diff both - pessimistic", + slice1: []int{1, 2, 3, 4, 5, 6}, + slice2: []int{7, 8, 9, 10, 11, 12}, + want: []int{1, 2, 3, 4, 5, 6}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DiffLeft(tt.slice1, tt.slice2) + if tt.want == nil && got != nil { + t.Errorf("Expected nil result got %+v instead", got) + return + } + + if len(tt.want) != len(got) { + t.Errorf("Got %+v, want %+v", got, tt.want) + return + } + + for k, v := range tt.want { + if v != got[k] { + t.Errorf("Got %+v, want %+v", got, tt.want) + } + } + }) + } +} + +func BenchmarkDiffLeft(b *testing.B) { + slice1 := []int{1, 2, 3, 4, 5, 6} + slice2 := []int{5, 6, 7, 8} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + DiffLeft(slice1, slice2) + } +} + +func BenchmarkDiffLeft_MoreValues_Random(b *testing.B) { + var slice1, slice2 []int + for i := 0; i < 300; i++ { + slice1 = append(slice1, i) + } + + for i := 0; i < 50; i++ { + slice2 = append(slice2, i) + } + rand.Shuffle(len(slice1), func(i, j int) { slice1[i], slice1[j] = slice1[j], slice1[i] }) + rand.Shuffle(len(slice2), func(i, j int) { slice2[i], slice2[j] = slice2[j], slice2[i] }) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + DiffLeft(slice1, slice2) + } +} + +func TestDiffRight(t *testing.T) { + tests := []struct { + name string + slice1 []int + slice2 []int + want []int + }{ + { + name: "diff left slice", + slice1: []int{1, 2, 3}, + slice2: []int{3}, + want: []int{}, + }, + { + name: "diff right slice", + slice1: []int{3}, + slice2: []int{1, 2, 3}, + want: []int{1, 2}, + }, + { + name: "diff both", + slice1: []int{3, 4, 5}, + slice2: []int{1, 2, 3}, + want: []int{1, 2}, + }, + { + name: "diff both slices - same values", + slice1: []int{1, 2, 3, 4, 5, 6}, + slice2: []int{1, 2, 3, 4, 5, 6}, + want: []int{}, + }, + { + name: "diff empty", + slice1: []int{}, + slice2: []int{}, + want: []int{}, + }, + { + name: "diff both - pessimistic", + slice1: []int{1, 2, 3, 4, 5, 6}, + slice2: []int{7, 8, 9, 10, 11, 12}, + want: []int{7, 8, 9, 10, 11, 12}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DiffRight(tt.slice1, tt.slice2) + if tt.want == nil && got != nil { + t.Errorf("Expected nil result got %+v instead", got) + return + } + + if len(tt.want) != len(got) { + t.Errorf("Got %+v, want %+v", got, tt.want) + return + } + + for k, v := range tt.want { + if v != got[k] { + t.Errorf("Got %+v, want %+v", got, tt.want) + } + } + }) + } +} + +func BenchmarkDiffRight(b *testing.B) { + slice1 := []int{1, 2, 3, 4, 5, 6} + slice2 := []int{5, 6, 7, 8} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + DiffRight(slice1, slice2) + } +} + +func BenchmarkDiffRight_MoreValues_Random(b *testing.B) { + var slice1, slice2 []int + for i := 0; i < 300; i++ { + slice1 = append(slice1, i) + } + + for i := 0; i < 50; i++ { + slice2 = append(slice2, i) + } + rand.Shuffle(len(slice1), func(i, j int) { slice1[i], slice1[j] = slice1[j], slice1[i] }) + rand.Shuffle(len(slice2), func(i, j int) { slice2[i], slice2[j] = slice2[j], slice2[i] }) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + DiffRight(slice2, slice1) + } +} diff --git a/gofp/map.go b/gofp/map.go index a516623..ff00f0d 100644 --- a/gofp/map.go +++ b/gofp/map.go @@ -8,3 +8,13 @@ func Map[T, V any](list []T, fn func(T) V) (res []V) { return } + +// MapMultiple apply a function to all elements on an array. +// Applied function should return multiple elements out of single list element. +func MapMultiple[T, V any](list []T, fn func(T) []V) (res []V) { + for i := range list { + res = append(res, fn(list[i])...) + } + + return +} diff --git a/gofp/map_test.go b/gofp/map_test.go new file mode 100644 index 0000000..0fc2a5b --- /dev/null +++ b/gofp/map_test.go @@ -0,0 +1,159 @@ +package gofp_test + +import ( + "strconv" + "testing" + + . "github.com/msales/gox/gofp" +) + +func TestMap_Int64ToIn32(t *testing.T) { + got := Map([]int64{1, 2, 3}, func(val int64) int32 { + return int32(val) + }) + + want := []int32{1, 2, 3} + + if len(want) != len(got) { + t.Errorf("Got %+v, want %+v", got, want) + return + } + + for k, v := range want { + if v != got[k] { + t.Errorf("Got %+v, want %+v", got, want) + } + } +} + +func TestMap_StringToStruct(t *testing.T) { + got := Map([]string{"1", "2", "3"}, func(val string) testID { + id, _ := strconv.ParseInt(val, 10, 64) + return testID{ID: id} + }) + + want := []testID{ + {ID: 1}, + {ID: 2}, + {ID: 3}, + } + + if len(want) != len(got) { + t.Errorf("Got %+v, want %+v", got, want) + return + } + + for k, v := range want { + if v != got[k] { + t.Errorf("Got %+v, want %+v", got, want) + } + } +} + +func BenchmarkMap_Int64ToIn32(b *testing.B) { + original := []int64{1, 2, 3, 4, 5, 6} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + Map(original, func(val int64) int32 { + return int32(val) + }) + } +} + +func BenchmarkMap_StringToStruct(b *testing.B) { + original := []string{"1", "2", "3"} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + Map(original, func(val string) testID { + id, _ := strconv.ParseInt(val, 10, 64) + return testID{ID: id} + }) + } +} + +func TestMapMultiple_Int64ToIn32(t *testing.T) { + got := MapMultiple([]int64{1, 2, 3}, func(val int64) []int32 { + return []int32{ + int32(val), + int32(val), + } + }) + + want := []int32{1, 1, 2, 2, 3, 3} + + if len(want) != len(got) { + t.Errorf("Got %+v, want %+v", got, want) + return + } + + for k, v := range want { + if v != got[k] { + t.Errorf("Got %+v, want %+v", got, want) + } + } +} + +func TestMapMultiple_StringToStruct(t *testing.T) { + got := MapMultiple([]string{"1", "2", "3"}, func(val string) []testID { + id, _ := strconv.ParseInt(val, 10, 64) + return []testID{ + {ID: id}, + {ID: id}, + } + }) + + want := []testID{ + {ID: 1}, + {ID: 1}, + {ID: 2}, + {ID: 2}, + {ID: 3}, + {ID: 3}, + } + + if len(want) != len(got) { + t.Errorf("Got %+v, want %+v", got, want) + return + } + + for k, v := range want { + if v != got[k] { + t.Errorf("Got %+v, want %+v", got, want) + } + } +} + +func BenchmarkMapMultiple_Int64ToIn32(b *testing.B) { + original := []int64{1, 2, 3, 4, 5, 6} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + MapMultiple(original, func(val int64) []int32 { + return []int32{ + int32(val), + int32(val), + } + }) + } +} + +func BenchmarkMapMultiple_StringToStruct(b *testing.B) { + original := []string{"1", "2", "3"} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + MapMultiple(original, func(val string) []testID { + id, _ := strconv.ParseInt(val, 10, 64) + return []testID{ + {ID: id}, + {ID: id}, + } + }) + } +} + +type testID struct { + ID int64 +} diff --git a/gofp/reduce.go b/gofp/reduce.go index c16f7b9..e19e1c6 100644 --- a/gofp/reduce.go +++ b/gofp/reduce.go @@ -4,7 +4,7 @@ package gofp // // Example: // intSlice := int {1, 2, 3, 4, 5} -// resp := utils.Reduce(intSlice, func(accum, item int) int { return accum + item }, 0) +// resp := gofp.Reduce(intSlice, func(accum, item int) int { return accum + item }, 0) // fmt.Println(resp) // ---> 15 func Reduce[T, V any](list []T, fn func(V, T) V, initValue V) V { acc := initValue diff --git a/gofp/reduce_test.go b/gofp/reduce_test.go new file mode 100644 index 0000000..d9bac1c --- /dev/null +++ b/gofp/reduce_test.go @@ -0,0 +1,46 @@ +package gofp_test + +import ( + "testing" + + . "github.com/msales/gox/gofp" +) + +func TestReduce(t *testing.T) { + tests := []struct { + name string + init int64 + slice []int64 + want int64 + }{ + { + name: "reduce multiple elements", + init: 0, + slice: []int64{1, 2, 3, 4, 5, 6}, + want: 21, + }, + { + name: "reduce multiple elements with init value", + init: 1, + slice: []int64{2137, 666}, + want: 2804, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Reduce(tt.slice, func(accum, item int64) int64 { return accum + item }, tt.init) + if tt.want != got { + t.Errorf("Got %+v, want %+v", got, tt.want) + } + }) + } +} + +func BenchmarkReduce(b *testing.B) { + original := []int64{1, 2, 3, 4, 5, 6} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + Reduce(original, func(accum, item int64) int64 { return accum + item }, 0) + } +}