Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DiscardElements helper transformer #28

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmp/cmpopts/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ func (ss sliceSorter) less(v reflect.Value, i, j int) bool {
// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
// • Total: if x != y, then either less(x, y) or less(y, x)
//
// SortMaps can be used in conjuction with EquateEmpty.
// SortMaps can be used in conjunction with EquateEmpty,
// but cannot be used with DiscardElements.
func SortMaps(less interface{}) cmp.Option {
vf := reflect.ValueOf(less)
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
Expand Down
90 changes: 90 additions & 0 deletions cmp/cmpopts/transform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2017, The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.

package cmpopts

import (
"fmt"
"reflect"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/internal/function"
)

// DiscardElements transforms slices and maps by discarding some elements.
// The remove function must be of the form "func(T) bool" where it reports true
// for any element that should be discarded. This transforms any slices and maps
// of type []V or map[K]V, where type V is assignable to type T.
//
// As an example, zero elements in a []MyStruct can be discarded with:
// DiscardElements(func(v MyStruct) bool { return v == MyStruct{} })
//
// DiscardElements can be used in conjunction with EquateEmpty,
// but cannot be used with SortMaps.
func DiscardElements(rm interface{}) cmp.Option {
vf := reflect.ValueOf(rm)
if !function.IsType(vf.Type(), function.Remove) || vf.IsNil() {
panic(fmt.Sprintf("invalid remove function: %T", rm))
}
d := discarder{vf.Type().In(0), vf}
return cmp.FilterValues(d.filter, cmp.Transformer("Discard", d.discard))
}

type discarder struct {
in reflect.Type // T
fnc reflect.Value // func(T) bool
}

func (d discarder) filter(x, y interface{}) bool {
vx := reflect.ValueOf(x)
vy := reflect.ValueOf(y)
if x == nil || y == nil || vx.Type() != vy.Type() ||
!(vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) ||
!vx.Type().Elem().AssignableTo(d.in) || vx.Len()+vy.Len() == 0 {
return false
}
ok := d.hasDiscardable(vx) || d.hasDiscardable(vy)
return ok
}
func (d discarder) hasDiscardable(v reflect.Value) bool {
switch v.Kind() {
case reflect.Slice:
for i := 0; i < v.Len(); i++ {
if d.fnc.Call([]reflect.Value{v.Index(i)})[0].Bool() {
return true
}
}
case reflect.Map:
for _, k := range v.MapKeys() {
if d.fnc.Call([]reflect.Value{v.MapIndex(k)})[0].Bool() {
return true
}
}
}
return false
}
func (d discarder) discard(x interface{}) interface{} {
src := reflect.ValueOf(x)
switch src.Kind() {
case reflect.Slice:
dst := reflect.MakeSlice(src.Type(), 0, src.Len())
for i := 0; i < src.Len(); i++ {
v := src.Index(i)
if !d.fnc.Call([]reflect.Value{v})[0].Bool() {
dst = reflect.Append(dst, v)
}
}
return dst.Interface()
case reflect.Map:
dst := reflect.MakeMap(src.Type())
for _, k := range src.MapKeys() {
v := src.MapIndex(k)
if !d.fnc.Call([]reflect.Value{v})[0].Bool() {
dst.SetMapIndex(k, v)
}
}
return dst.Interface()
}
panic("not a slice or map") // Not possible due to FilterValues
}
115 changes: 115 additions & 0 deletions cmp/cmpopts/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,121 @@ func TestOptions(t *testing.T) {
opts: []cmp.Option{EquateEmpty()},
wantEqual: true,
reason: "equal because EquateEmpty equates empty slices",
}, {
label: "DiscardElements",
x: map[string]int{"foo": 5, "fizz": 0},
y: map[string]int{"foo": 5, "buzz": 0},
opts: []cmp.Option{
DiscardElements(func(v int) bool { return v == 0 }),
},
wantEqual: true,
reason: "equal because DiscardElements transforms the map to remove fizz and buzz",
}, {
label: "DiscardElements",
x: map[string]MyStruct{"alice": {A: []int{1, 2, 3}}, "bob": {}},
y: map[string]MyStruct{"alice": {A: []int{1, 2, 3}}, "charlie": {}},
opts: []cmp.Option{
DiscardElements(func(v MyStruct) bool { return reflect.DeepEqual(v, MyStruct{}) }),
},
wantEqual: true,
reason: "equal because DiscardElements transforms maps with non-comparable types",
}, {
label: "DiscardElements",
x: []interface{}{
nil, nil, "foo",
[]interface{}{"a", "b", 3, nil, []interface{}{
map[string]interface{}{"foo": nil, "number": 5, "map": map[string]interface{}{
"zero": nil,
"array": []interface{}{nil, 1, nil, 2, nil, 3},
}},
}, nil},
nil,
},
y: []interface{}{
"foo",
[]interface{}{"a", "b", 3, []interface{}{
map[string]interface{}{"bar": nil, "number": 5, "map": map[string]interface{}{
"array": []interface{}{1, 2, 3, nil, nil},
}},
nil, nil,
}},
nil, nil,
},
opts: []cmp.Option{
DiscardElements(func(v interface{}) bool { return v == nil }),
},
wantEqual: true,
reason: "equal because DiscardElements applies recursively to sub-slices and sub-maps",
}, {
label: "DiscardElements",
x: map[string]MyStruct{
"alice": {
A: []int{1, 2, 3},
C: map[time.Time]string{
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "birthday",
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "",
},
},
"bob": {},
},
y: map[string]MyStruct{
"alice": {
A: []int{1, 2, 3},
C: map[time.Time]string{
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "birthday",
time.Date(2020, time.November, 10, 23, 0, 0, 0, time.UTC): "",
time.Date(2030, time.November, 10, 23, 0, 0, 0, time.UTC): "",
},
},
"charlie": {},
},
opts: []cmp.Option{
DiscardElements(func(v MyStruct) bool { return cmp.Equal(v, MyStruct{}) }),
DiscardElements(func(v string) bool { return v == "" }),
},
wantEqual: true,
reason: "equal because multiple DiscardElements used",
}, {
label: "DiscardElements",
x: map[string]MyStruct{"alice": {A: []int{}, C: map[time.Time]string(nil)}, "bob": {}},
y: map[string]MyStruct{"alice": {A: []int(nil), C: map[time.Time]string{}}, "charlie": {}},
opts: []cmp.Option{
DiscardElements(func(v MyStruct) bool { return cmp.Equal(v, MyStruct{}) }),
DiscardElements(func(v string) bool { return v == "" }),
},
wantEqual: false,
reason: "not equal because empty slices are not the same",
}, {
label: "DiscardElements+EquateEmpty",
x: map[string]MyStruct{"alice": {A: []int{}, C: map[time.Time]string(nil)}, "bob": {}},
y: map[string]MyStruct{"alice": {A: []int(nil), C: map[time.Time]string{}}, "charlie": {}},
opts: []cmp.Option{
DiscardElements(func(v MyStruct) bool { return cmp.Equal(v, MyStruct{}) }),
DiscardElements(func(v string) bool { return v == "" }),
EquateEmpty(),
},
wantEqual: true,
reason: "equal because zero map values and empty slices are all equal",
}, {
label: "DiscardElements+EquateEmpty",
x: map[string][]int{"foo": {}},
y: map[string][]int{"foo": nil},
opts: []cmp.Option{
DiscardElements(func(v []int) bool { return v == nil }),
EquateEmpty(),
},
wantEqual: false,
reason: "not equal because DiscardElements only remove nil slice, but leaves the empty non-nil slice alone",
}, {
label: "DiscardElements+EquateEmpty",
x: map[string][]int{"foo": {}},
y: map[string][]int{"foo": nil},
opts: []cmp.Option{
DiscardElements(func(v []int) bool { return len(v) == 0 }),
EquateEmpty(),
},
wantEqual: true,
reason: "equal because DiscardElements uses a more liberal definition of equal to zero",
}, {
label: "SortSlices",
x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
Expand Down
6 changes: 6 additions & 0 deletions cmp/internal/function/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ const (

ttbFunc // func(T, T) bool
tibFunc // func(T, I) bool
tbFunc // func(T) bool
trFunc // func(T) R

Equal = ttbFunc // func(T, T) bool
EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool
Transformer = trFunc // func(T) R
ValueFilter = ttbFunc // func(T, T) bool
Less = ttbFunc // func(T, T) bool
Remove = tbFunc // func(T) bool
)

var boolType = reflect.TypeOf(true)
Expand All @@ -40,6 +42,10 @@ func IsType(t reflect.Type, ft funcType) bool {
if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType {
return true
}
case tbFunc: // func(T) bool
if ni == 1 && no == 1 && t.Out(0) == boolType {
return true
}
case trFunc: // func(T) R
if ni == 1 && no == 1 {
return true
Expand Down