From b5137de5ffbf79a4bacc4aa47d642e1d9e5ca78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 5 Nov 2018 18:50:11 +0100 Subject: [PATCH] tpl/collections: Add collections.Complement Fixes #5400 --- tpl/collections/complement.go | 72 +++++++++++++++++++++++++ tpl/collections/complement_test.go | 84 ++++++++++++++++++++++++++++++ tpl/collections/init.go | 7 +++ tpl/collections/reflect_helpers.go | 18 +++++++ 4 files changed, 181 insertions(+) create mode 100644 tpl/collections/complement.go create mode 100644 tpl/collections/complement_test.go diff --git a/tpl/collections/complement.go b/tpl/collections/complement.go new file mode 100644 index 00000000000..d975fabac4d --- /dev/null +++ b/tpl/collections/complement.go @@ -0,0 +1,72 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "reflect" +) + +// Complement gives the elements in the last element of seqs that are not in +// any of the others. +// All elements of seqs must be slices or arrays of comparable types. +// +// The reasoning behind this rather clumsy API is so we can do this in the templates: +// {{ $c := .Pages | complement $last4 }} +func (ns *Namespace) Complement(seqs ...interface{}) (interface{}, error) { + if len(seqs) < 2 { + return nil, errors.New("complement needs at least two arguments") + } + + universe := seqs[len(seqs)-1] + as := seqs[:len(seqs)-1] + + aset := make(map[interface{}]struct{}) + + for _, av := range as { + v := reflect.ValueOf(av) + switch v.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < v.Len(); i++ { + ev, _ := indirectInterface(v.Index(i)) + if !ev.Type().Comparable() { + return nil, errors.New("elements in complement must be comparable") + } + + aset[normalize(ev)] = struct{}{} + } + default: + return nil, fmt.Errorf("arguments to complement must be slices or arrays") + } + } + + v := reflect.ValueOf(universe) + switch v.Kind() { + case reflect.Array, reflect.Slice: + sl := reflect.MakeSlice(v.Type(), 0, 0) + for i := 0; i < v.Len(); i++ { + ev, _ := indirectInterface(v.Index(i)) + if !ev.Type().Comparable() { + return nil, errors.New("elements in complement must be comparable") + } + if _, found := aset[normalize(ev)]; !found { + sl = reflect.Append(sl, ev) + } + } + return sl.Interface(), nil + default: + return nil, fmt.Errorf("arguments to complement must be slices or arrays") + } +} diff --git a/tpl/collections/complement_test.go b/tpl/collections/complement_test.go new file mode 100644 index 00000000000..4cae7556fee --- /dev/null +++ b/tpl/collections/complement_test.go @@ -0,0 +1,84 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "fmt" + "reflect" + "testing" + + "github.com/gohugoio/hugo/deps" + + "github.com/stretchr/testify/require" +) + +func TestComplement(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + ns := New(&deps.Deps{}) + + s1 := []TstX{TstX{A: "a"}, TstX{A: "b"}, TstX{A: "d"}, TstX{A: "e"}} + s2 := []TstX{TstX{A: "b"}, TstX{A: "e"}} + + xa, xd := &TstX{A: "a"}, &TstX{A: "d"} + + sp1 := []*TstX{xa, &TstX{A: "b"}, xd, &TstX{A: "e"}} + sp2 := []*TstX{&TstX{A: "b"}, &TstX{A: "e"}} + + for i, test := range []struct { + s interface{} + t []interface{} + expected interface{} + }{ + {[]string{"a", "b", "c"}, []interface{}{[]string{"c", "d"}}, []string{"a", "b"}}, + {[]string{"a", "b", "c"}, []interface{}{[]string{"c", "d"}, []string{"a", "b"}}, []string{}}, + {[]interface{}{"a", "b", nil}, []interface{}{[]string{"a", "d"}}, []interface{}{"b", nil}}, + {[]int{1, 2, 3, 4, 5}, []interface{}{[]int{1, 3}, []string{"a", "b"}, []int{1, 2}}, []int{4, 5}}, + {[]int{1, 2, 3, 4, 5}, []interface{}{[]int64{1, 3}}, []int{2, 4, 5}}, + {s1, []interface{}{s2}, []TstX{TstX{A: "a"}, TstX{A: "d"}}}, + {sp1, []interface{}{sp2}, []*TstX{xa, xd}}, + + // Errors + {[]string{"a", "b", "c"}, []interface{}{"error"}, false}, + {"error", []interface{}{[]string{"c", "d"}, []string{"a", "b"}}, false}, + {[]string{"a", "b", "c"}, []interface{}{[][]string{[]string{"c", "d"}}}, false}, + {[]interface{}{[][]string{[]string{"c", "d"}}}, []interface{}{[]string{"c", "d"}, []string{"a", "b"}}, false}, + } { + + errMsg := fmt.Sprintf("[%d]", i) + + args := append(test.t, test.s) + + result, err := ns.Complement(args...) + + if b, ok := test.expected.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + + if !reflect.DeepEqual(test.expected, result) { + t.Fatalf("%s got\n%T: %v\nexpected\n%T: %v", errMsg, result, result, test.expected, test.expected) + } + } + + _, err := ns.Complement() + assert.Error(err) + _, err = ns.Complement([]string{"a", "b"}) + assert.Error(err) + +} diff --git a/tpl/collections/init.go b/tpl/collections/init.go index 879e4738c4a..569932c0806 100644 --- a/tpl/collections/init.go +++ b/tpl/collections/init.go @@ -39,6 +39,13 @@ func init() { [][2]string{}, ) + ns.AddMethodMapping(ctx.Complement, + []string{"complement"}, + [][2]string{ + {`{{ slice "a" "b" "c" "d" "e" "f" | complement (slice "b" "c") (slice "d" "e") }}`, `[a f]`}, + }, + ) + ns.AddMethodMapping(ctx.Delimit, []string{"delimit"}, [][2]string{ diff --git a/tpl/collections/reflect_helpers.go b/tpl/collections/reflect_helpers.go index 643a0a7e56e..074396479da 100644 --- a/tpl/collections/reflect_helpers.go +++ b/tpl/collections/reflect_helpers.go @@ -41,6 +41,24 @@ func numberToFloat(v reflect.Value) (float64, error) { } } +// normalizes different numeric types to make them comparable. +// If not, any pointer will be unwrapped. +func normalize(v reflect.Value) interface{} { + k := v.Kind() + + switch { + case isNumber(k): + f, err := numberToFloat(v) + if err == nil { + return f + } + case k == reflect.Ptr: + v = v.Elem() + } + + return v.Interface() +} + // There are potential overflows in this function, but the downconversion of // int64 etc. into int8 etc. is coming from the synthetic unit tests for Union etc. // TODO(bep) We should consider normalizing the slices to int64 etc.