diff --git a/ext/lists.go b/ext/lists.go index 16723936..b6f6251b 100644 --- a/ext/lists.go +++ b/ext/lists.go @@ -40,6 +40,22 @@ var comparableTypes = []*cel.Type{ // Lists returns a cel.EnvOption to configure extended functions for list manipulation. // As a general note, all indices are zero-based. // +// # Distinct +// +// Introduced in version: 2 +// +// Returns the distinct elements of a list. If the element type is not comparable +// or the element types are not the same, the function will produce an error. +// +// .distinct() -> +// T in {int, uint, double, bool, duration, timestamp, string, bytes} +// +// Examples: +// +// [1, 2, 2, 3, 3, 3].distinct() // return [1, 2, 3] +// ["b", "b", "c", "a", "c"].distinct() // return ["b", "c", "a"] +// [1, "b", 2, "b"].distinct() // return [1, "b", 2] +// // # Range // // Introduced in version: 2 @@ -269,6 +285,19 @@ func (lib listsLib) CompileOptions() []cel.EnvOption { }), ), )) + opts = append(opts, cel.Function("distinct", + cel.MemberOverload("list_distinct", + []*cel.Type{listType}, listType, + cel.FunctionBinding(func(args ...ref.Val) ref.Val { + list := args[0].(traits.Lister) + result, err := distinctList(list) + if err != nil { + return types.WrapErr(err) + } + return result + }), + ), + )) } return opts @@ -371,6 +400,24 @@ func sortList(list traits.Lister) (ref.Val, error) { return types.DefaultTypeAdapter.NativeToValue(sorted), nil } +func distinctList(list traits.Lister) (ref.Val, error) { + listLength := list.Size().(types.Int) + if listLength == 0 { + return list, nil + } + uniqueList := make([]ref.Val, 0, listLength) + seen := make(map[ref.Val]bool, listLength) + for i := types.IntZero; i < listLength; i++ { + val := list.Get(i) + if !seen[val] { + uniqueList = append(uniqueList, val) + seen[val] = true + } + } + + return types.DefaultTypeAdapter.NativeToValue(uniqueList), nil +} + func templatedOverloads(types []*cel.Type, template func(t *cel.Type) cel.FunctionOpt) []cel.FunctionOpt { overloads := make([]cel.FunctionOpt, len(types)) for i, t := range types { diff --git a/ext/lists_test.go b/ext/lists_test.go index 73a5cdfa..f233e28e 100644 --- a/ext/lists_test.go +++ b/ext/lists_test.go @@ -50,9 +50,16 @@ func TestLists(t *testing.T) { {expr: `[1,2,[],[],[3,4]].flatten() == [1,2,3,4]`}, {expr: `[1,[2,[3,4]]].flatten(2) == [1,2,3,4]`}, {expr: `[1,[2,[3,[4]]]].flatten(-1) == [1,2,3,4]`, err: "level must be non-negative"}, + {expr: `[].sort() == []`}, + {expr: `[1].sort() == [1]`}, {expr: `[4, 3, 2, 1].sort() == [1, 2, 3, 4]`}, {expr: `["d", "a", "b", "c"].sort() == ["a", "b", "c", "d"]`}, {expr: `["d", 3, 2, "c"].sort() == ["a", "b", "c", "d"]`, err: "list elements must have the same type"}, + {expr: `[].distinct() == []`}, + {expr: `[1].distinct() == [1]`}, + {expr: `[-2, 5, -2, 1, 1, 5, -2, 1].distinct() == [-2, 5, 1]`}, + {expr: `['c', 'a', 'a', 'b', 'a', 'b', 'c', 'c'].distinct() == ['c', 'a', 'b']`}, + {expr: `[1, 2.0, "c", 3, "c", 1].distinct() == [1, 2.0, "c", 3]`}, } env := testListsEnv(t)