diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f3b71bb..a0da047 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,7 +26,7 @@ jobs: run: go version - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@2022.1 + run: go install honnef.co/go/tools/cmd/staticcheck@2022.1.3 - name: Install cover run: go get -u golang.org/x/tools/cmd/cover @@ -50,4 +50,4 @@ jobs: run: | cat profile_full.cov | grep -v .pb.go | grep -v mock | grep -v test > profile.cov; goveralls -coverprofile=profile.cov -service=github || true; - + diff --git a/gofp/diff.go b/gofp/diff.go index ef59a3b..e8f6f27 100644 --- a/gofp/diff.go +++ b/gofp/diff.go @@ -1,25 +1,33 @@ package gofp // SymDiff returns symmetric difference of 2 slices. +// The function returns a new slice containing the elements that are in slice1 or slice2 but not in both. // 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)) + checks1 := make(map[T]bool) + checks2 := make(map[T]bool) + + // Track unique elements in slice1 and slice2 for _, v := range slice1 { - checks[v]++ - idx = append(idx, v) + checks1[v] = true } for _, v := range slice2 { - checks[v]++ - if checks[v] == 1 { - idx = append(idx, v) + checks2[v] = true + } + + outer := make([]T, 0) + + // Add elements in slice1 but not in slice2 + for v := range checks1 { + if !checks2[v] { + outer = append(outer, v) } } - outer := make([]T, 0, len(slice1)+len(slice2)) - for _, id := range idx { - if checks[id] == 1 { - outer = append(outer, id) + // Add elements in slice2 but not in slice1 + for v := range checks2 { + if !checks1[v] { + outer = append(outer, v) } } @@ -27,19 +35,19 @@ func SymDiff[T comparable](slice1, slice2 []T) []T { } // DiffLeft returns left diff of 2 slices. +// The function returns a new slice containing the elements that are in slice1 but not in slice2. func DiffLeft[T comparable](slice1, slice2 []T) []T { - checks := make(map[T]int8, len(slice1)) - for _, v := range slice1 { - checks[v]++ - } + checks := make(map[T]bool) for _, v := range slice2 { - checks[v]++ + checks[v] = true } - outer := make([]T, 0, len(slice1)) - for _, id := range slice1 { - if checks[id] == 1 { - outer = append(outer, id) + var outer []T + used := make(map[T]bool) + for _, v := range slice1 { + if !checks[v] && !used[v] { + used[v] = true + outer = append(outer, v) } } @@ -47,6 +55,7 @@ func DiffLeft[T comparable](slice1, slice2 []T) []T { } // DiffRight returns right diff of 2 slices. +// The function returns a new slice containing the elements that are in slice2 but not in slice1. func DiffRight[T comparable](slice1, slice2 []T) []T { return DiffLeft(slice2, slice1) } diff --git a/gofp/diff_test.go b/gofp/diff_test.go index 86fef07..862c994 100644 --- a/gofp/diff_test.go +++ b/gofp/diff_test.go @@ -3,6 +3,8 @@ package gofp import ( "math/rand" "testing" + + "github.com/stretchr/testify/assert" ) func TestSymDiff(t *testing.T) { @@ -48,6 +50,12 @@ func TestSymDiff(t *testing.T) { slice2: []int{7, 8, 9, 10, 11, 12}, want: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, }, + { + name: "repeated values", + slice1: []int{1, 1, 1, 1, 2, 2, 2, 1}, + slice2: []int{3, 3, 3, 3, 3, 3, 3}, + want: []int{1, 2, 3}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -62,10 +70,8 @@ func TestSymDiff(t *testing.T) { return } - for k, v := range tt.want { - if v != got[k] { - t.Errorf("Got %+v, want %+v", got, tt.want) - } + for _, v := range tt.want { + assert.Contains(t, got, v, "Got %+v, want %+v") } }) } @@ -170,6 +176,18 @@ func TestDiffLeft(t *testing.T) { slice2: []int{7, 8, 9, 10, 11, 12}, want: []int{1, 2, 3, 4, 5, 6}, }, + { + 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}, + }, + { + name: "repeated values", + slice1: []int{1, 1, 1, 1, 2, 2, 2, 1}, + slice2: []int{3, 3, 3, 3, 3, 3, 3}, + want: []int{1, 2}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -268,6 +286,12 @@ func TestDiffRight(t *testing.T) { slice2: []int{7, 8, 9, 10, 11, 12}, want: []int{7, 8, 9, 10, 11, 12}, }, + { + name: "repeated values", + slice1: []int{3, 3, 3, 3, 3, 3, 3}, + slice2: []int{1, 1, 1, 1, 2, 2, 2, 1}, + want: []int{1, 2}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/gofp/diff_with_key_builder.go b/gofp/diff_with_key_builder.go new file mode 100644 index 0000000..fe59219 --- /dev/null +++ b/gofp/diff_with_key_builder.go @@ -0,0 +1,171 @@ +package gofp + +// KeyBuilder is a type for a function that maps a value of type T to a comparable value of type C. +// This can be used for constructing a comparable key from an object, allowing it to be used in comparison operations. +type KeyBuilder[T any, C comparable] func(T) C + +// SymDiffWithKeyBuilder calculates the symmetric difference between two slices. +// It uses a KeyBuilder function to map values in the slices to a comparable key, which is used for the comparison. +// The function returns a new slice containing the elements that are in slice1 or slice2 but not in both. +// +// Notes: +// - This function doesn't return an ordered response. +// - The KeyBuilder function is used to identify unique elements in the slices for comparison. +// Therefore, it's important that the KeyBuilder function produces unique keys for unique elements. +// If the keys are not unique, the function may not provide accurate results. +// For example, consider two slices of a struct with attributes 'Name' and 'Age'. If the KeyBuilder function +// uses only the 'Name' attribute for creating the key, two elements with the same 'Name' but different 'Age' +// will be considered identical, which may not be the intended behavior. +// +// Example: +// +// slice1 := []Person{ +// {Name: "Alice", Age: 25}, +// {Name: "Bob", Age: 30}, +// {Name: "Alice", Age: 40}, +// } +// slice2 := []Person{ +// {Name: "Charlie", Age: 35}, +// } +// kb := func(p Person) string { +// return p.Name +// } +// result := SymDiffWithKeyBuilder(slice1, slice2, kb) +// +// The 'result' will be: +// +// []Person{ +// {Name: "Alice", Age: 25}, +// {Name: "Bob", Age: 30}, +// {Name: "Charlie", Age: 35}, +// } +// +// Note that for the 'Alice' element, the 'Age' attribute of 25 was used and the 40 was omitted. +func SymDiffWithKeyBuilder[T any, C comparable](slice1, slice2 []T, kb KeyBuilder[T, C]) []T { + checks1, checks2 := make(Set[C]), make(Set[C]) + valueByKey := make(map[C]T) + for _, v := range slice1 { + k := kb(v) + checks1.Add(k) + valueByKey[k] = v + } + + for _, v := range slice2 { + checks2.Add(kb(v)) + k := kb(v) + if _, ok := valueByKey[k]; !ok { + valueByKey[k] = v + } + } + + var result []T + for k := range checks1 { + if !checks2.Has(k) { + result = append(result, valueByKey[k]) + } + } + for k := range checks2 { + if !checks1.Has(k) { + result = append(result, valueByKey[k]) + } + } + + return result +} + +// DiffLeftWithKeyBuilder calculates the difference between two slices, i.e., elements that are in the first slice, but not in the second. +// It uses a KeyBuilder function to map values in the slices to a comparable key, which is used for the comparison. +// The function returns a new slice containing the elements that are in slice1 but not in slice2. +// +// Notes: +// - This function does not retain the order of elements in the original slices. The order of elements in the resulting slice depends on the order of iteration over slice1. +// - The KeyBuilder function is used to identify unique elements in the slices for comparison. +// Therefore, it's important that the KeyBuilder function produces unique keys for unique elements. +// If the keys are not unique, the function may not provide accurate results. +// For example, consider two slices of a struct with attributes 'Name' and 'Age'. If the KeyBuilder function +// uses only the 'Name' attribute for creating the key, two elements with the same 'Name' but different 'Age' +// will be considered identical, which may not be the intended behavior. +// +// Example: +// +// slice1 := []Person{ +// {Name: "Alice", Age: 25}, +// {Name: "Bob", Age: 30}, +// {Name: "Alice", Age: 40}, +// {Name: "Charlie", Age: 35}, +// } +// slice2 := []Person{ +// {Name: "Charlie", Age: 20}, +// } +// kb := func(p Person) string { +// return p.Name +// } +// result := DiffLeftWithKeyBuilder(slice1, slice2, kb) +// +// The 'result' will be: +// +// []Person{ +// {Name: "Bob", Age: 30}, +// {Name: "Alice", Age: 25}, +// } +// +// Note that for the 'Alice' element, the 'Age' attribute of 25 was used and the 40 was omitted. +func DiffLeftWithKeyBuilder[T any, C comparable](slice1, slice2 []T, kb KeyBuilder[T, C]) []T { + checks := make(Set[C]) + for _, v := range slice2 { + checks.Add(kb(v)) + } + + outer := make([]T, 0) + used := make(Set[C]) + for _, v := range slice1 { + k := kb(v) + if !checks.Has(k) && !used.Has(k) { + outer = append(outer, v) + used.Add(k) + } + } + + return outer +} + +// DiffRightWithKeyBuilder calculates the difference between two slices, i.e., elements that are in the second slice, but not in the first. +// It uses a KeyBuilder function to map values in the slices to a comparable key, which is used for the comparison. +// The function returns a new slice containing the elements that are in slice2 but not in slice1. +// +// Notes: +// - This function does not retain the order of elements in the original slices. The order of elements in the resulting slice depends on the order of iteration over slice2. +// - The KeyBuilder function is used to identify unique elements in the slices for comparison. +// Therefore, it's important that the KeyBuilder function produces unique keys for unique elements. +// If the keys are not unique, the function may not provide accurate results. +// For example, consider two slices of a struct with attributes 'Name' and 'Age'. If the KeyBuilder function +// uses only the 'Name' attribute for creating the key, two elements with the same 'Name' but different 'Age' +// will be considered identical, which may not be the intended behavior. +// +// Example: +// +// slice1 := []Person{ +// {Name: "Charlie", Age: 20}, +// } +// slice2 := []Person{ +// {Name: "Alice", Age: 25}, +// {Name: "Bob", Age: 30}, +// {Name: "Alice", Age: 40}, +// {Name: "Charlie", Age: 35}, +// } +// kb := func(p Person) string { +// return p.Name +// } +// result := DiffRightWithKeyBuilder(slice1, slice2, kb) +// +// The 'result' will be: +// +// []Person{ +// {Name: "Bob", Age: 30}, +// {Name: "Alice", Age: 25}, +// } +// +// Note that for the 'Alice' element, the 'Age' attribute of 25 was used and the 40 was omitted. +func DiffRightWithKeyBuilder[T any, C comparable](slice1, slice2 []T, kb KeyBuilder[T, C]) []T { + return DiffLeftWithKeyBuilder(slice2, slice1, kb) +} diff --git a/gofp/diff_with_key_builder_test.go b/gofp/diff_with_key_builder_test.go new file mode 100644 index 0000000..8b83a24 --- /dev/null +++ b/gofp/diff_with_key_builder_test.go @@ -0,0 +1,159 @@ +package gofp_test + +import ( + "reflect" + "testing" + + "github.com/msales/gox/gofp" + "github.com/stretchr/testify/assert" +) + +func TestDiffLeftWithKeyBuilder(t *testing.T) { + // Define the test cases + tests := []struct { + name string + slice1 []int + slice2 []int + kb gofp.KeyBuilder[int, int] + want []int + }{ + { + name: "disjoint slices", + slice1: []int{1, 2, 3}, + slice2: []int{4, 5, 6}, + kb: func(i int) int { return i }, + want: []int{1, 2, 3}, + }, + { + name: "overlapping slices", + slice1: []int{1, 2, 3}, + slice2: []int{3, 4, 5}, + kb: func(i int) int { return i }, + want: []int{1, 2}, + }, + { + name: "identical slices", + slice1: []int{1, 2, 3}, + slice2: []int{1, 2, 3}, + kb: func(i int) int { return i }, + want: []int{}, + }, + { + name: "repeated values", + slice1: []int{1, 1, 1, 1, 2, 2, 2, 1}, + slice2: []int{3, 3, 3, 3, 3, 3, 3}, + kb: func(i int) int { return i }, + want: []int{1, 2}, + }, + } + + // Run the tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := gofp.DiffLeftWithKeyBuilder(tt.slice1, tt.slice2, tt.kb) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("DiffLeftWithKeyBuilder() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDiffRightWithKeyBuilder(t *testing.T) { + // Define the test cases + tests := []struct { + name string + slice1 []int + slice2 []int + kb gofp.KeyBuilder[int, int] + want []int + }{ + { + name: "disjoint slices", + slice1: []int{4, 5, 6}, + slice2: []int{1, 2, 3}, + kb: func(i int) int { return i }, + want: []int{1, 2, 3}, + }, + { + name: "overlapping slices", + slice1: []int{3, 4, 5}, + slice2: []int{1, 2, 3}, + kb: func(i int) int { return i }, + want: []int{1, 2}, + }, + { + name: "identical slices", + slice1: []int{1, 2, 3}, + slice2: []int{1, 2, 3}, + kb: func(i int) int { return i }, + want: []int{}, + }, + { + name: "repeated values", + slice1: []int{3, 3, 3, 3, 3, 3, 3}, + slice2: []int{1, 1, 1, 1, 2, 2, 2, 1}, + kb: func(i int) int { return i }, + want: []int{1, 2}, + }, + } + + // Run the tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := gofp.DiffRightWithKeyBuilder(tt.slice1, tt.slice2, tt.kb) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("DiffLeftWithKeyBuilder() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSymDiffWithKeyBuilder(t *testing.T) { + // define the test cases + tests := []struct { + name string + slice1 []int + slice2 []int + kb gofp.KeyBuilder[int, int] + want []int + }{ + { + name: "disjoint slices", + slice1: []int{1, 2, 3}, + slice2: []int{4, 5, 6}, + kb: func(i int) int { return i }, + want: []int{1, 2, 3, 4, 5, 6}, + }, + { + name: "overlapping slices", + slice1: []int{1, 2, 3}, + slice2: []int{3, 4, 5}, + kb: func(i int) int { return i }, + want: []int{1, 2, 4, 5}, + }, + { + name: "identical slices", + slice1: []int{1, 2, 3}, + slice2: []int{1, 2, 3}, + kb: func(i int) int { return i }, + want: []int{}, + }, + { + name: "repeated values", + slice1: []int{1, 1, 1, 1, 2, 2, 2, 1}, + slice2: []int{3, 3, 3, 3, 3, 3, 3}, + kb: func(i int) int { return i }, + want: []int{1, 2, 3}, + }, + } + + // Run the tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := gofp.SymDiffWithKeyBuilder(tt.slice1, tt.slice2, tt.kb) + for _, v := range tt.want { + assert.Contains(t, got, v, "Got %+v, want %+v") + } + }) + } +}