Skip to content

Commit

Permalink
[TASK] TRK-1612 Add DiffWithKeyBuilder functions to gofp library (#28)
Browse files Browse the repository at this point in the history
* [TASK] TRK-1612 Add DiffWithKeyBuilder functions to gofp library

* [TASK] TRK-1612 Update staticcheck version

* [TASK] TRK-1612 Fix test with repeated elements in diff methods
  • Loading branch information
hsequeda authored Jun 8, 2023
1 parent 986edc2 commit 8d09eb0
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 26 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
run: go version

- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/[email protected]
run: go install honnef.co/go/tools/cmd/[email protected].3

- name: Install cover
run: go get -u golang.org/x/tools/cmd/cover
Expand All @@ -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;
49 changes: 29 additions & 20 deletions gofp/diff.go
Original file line number Diff line number Diff line change
@@ -1,52 +1,61 @@
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)
}
}

return outer
}

// 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)
}
}

return outer
}

// 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)
}
32 changes: 28 additions & 4 deletions gofp/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package gofp
import (
"math/rand"
"testing"

"github.com/stretchr/testify/assert"
)

func TestSymDiff(t *testing.T) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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")
}
})
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
171 changes: 171 additions & 0 deletions gofp/diff_with_key_builder.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 8d09eb0

Please sign in to comment.