Skip to content

Commit

Permalink
ext: add list.sort() (#1021)
Browse files Browse the repository at this point in the history
* ext: add list.sort()
* ext: add overloads for the individual comparable types
* fix conformance test by using std sort instead of slices
* use singleton unary binding for sort function
* use traits.ListerType helper
* include version introducing list.sort in the docs

---------

Co-authored-by: Cezar Guimaraes <[email protected]>
  • Loading branch information
cezar-guimaraes and Cezar Guimaraes authored Sep 23, 2024
1 parent 6c98875 commit 45bd6f6
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 1 deletion.
19 changes: 18 additions & 1 deletion ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,23 @@ Examples:
[1,2,3,4].slice(1, 3) // return [2, 3]
[1,2,3,4].slice(2, 4) // return [3 ,4]

### Sort

**Introduced in version 2**

Sorts a list with comparable elements. If the element type is not comparable
or the element types are not the same, the function will produce an error.

<list(T)>.sort() -> <list(T)>
T in {int, uint, double, bool, duration, timestamp, string, bytes}

Examples:

[3, 2, 1].sort() // return [1, 2, 3]
["b", "c", "a"].sort() // return ["a", "b", "c"]
[1, "b"].sort() // error
[[1, 2, 3]].sort() // error

## Sets

Sets provides set relationship tests.
Expand Down Expand Up @@ -666,4 +683,4 @@ It can be located in Version 3 of strings.
Examples:

'gums'.reverse() // returns 'smug'
'John Smith'.reverse() // returns 'htimS nhoJ'
'John Smith'.reverse() // returns 'htimS nhoJ'
93 changes: 93 additions & 0 deletions ext/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package ext
import (
"fmt"
"math"
"sort"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/decls"
Expand All @@ -25,6 +26,17 @@ import (
"github.com/google/cel-go/common/types/traits"
)

var comparableTypes = []*cel.Type{
cel.IntType,
cel.UintType,
cel.DoubleType,
cel.BoolType,
cel.DurationType,
cel.TimestampType,
cel.StringType,
cel.BytesType,
}

// Lists returns a cel.EnvOption to configure extended functions for list manipulation.
// As a general note, all indices are zero-based.
// # Slice
Expand Down Expand Up @@ -54,6 +66,24 @@ import (
// [1,2,[],[],[3,4]].flatten() // return [1, 2, 3, 4]
// [1,[2,[3,[4]]]].flatten(2) // return [1, 2, 3, [4]]
// [1,[2,[3,[4]]]].flatten(-1) // error
//
// # Sort
//
// Introduced in version: 2
//
// Sorts a list with comparable elements. If the element type is not comparable
// or the element types are not the same, the function will produce an error.
//
// <list(T)>.sort() -> <list(T)>
// T in {int, uint, double, bool, duration, timestamp, string, bytes}
//
// Examples:
//
// [3, 2, 1].sort() // return [1, 2, 3]
// ["b", "c", "a"].sort() // return ["a", "b", "c"]
// [1, "b"].sort() // error
// [[1, 2, 3]].sort() // error

func Lists(options ...ListsOption) cel.EnvOption {
l := &listsLib{
version: math.MaxUint32,
Expand Down Expand Up @@ -159,6 +189,35 @@ func (lib listsLib) CompileOptions() []cel.EnvOption {
),
)
}
if lib.version >= 2 {
sortDecl := cel.Function("sort",
append(
templatedOverloads(comparableTypes, func(t *cel.Type) cel.FunctionOpt {
return cel.MemberOverload(
fmt.Sprintf("list_%s_sort", t.TypeName()),
[]*cel.Type{cel.ListType(t)}, cel.ListType(t),
)
}),
cel.SingletonUnaryBinding(
func(arg ref.Val) ref.Val {
list, ok := arg.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
sorted, err := sortList(list)
if err != nil {
return types.WrapErr(err)
}

return sorted
},
// List traits
traits.ListerType,
),
)...,
)
opts = append(opts, sortDecl)
}

return opts
}
Expand Down Expand Up @@ -215,3 +274,37 @@ func flatten(list traits.Lister, depth int64) ([]ref.Val, error) {

return newList, nil
}

func sortList(list traits.Lister) (ref.Val, error) {
listLength := list.Size().(types.Int)
if listLength == 0 {
return list, nil
}
elem := list.Get(types.IntZero)
if _, ok := elem.(traits.Comparer); !ok {
return nil, fmt.Errorf("list elements must be comparable")
}

sorted := make([]ref.Val, 0, listLength)
for i := types.IntZero; i < listLength; i++ {
val := list.Get(i)
if val.Type() != elem.Type() {
return nil, fmt.Errorf("list elements must have the same type")
}
sorted = append(sorted, val)
}

sort.Slice(sorted, func(i, j int) bool {
return sorted[i].(traits.Comparer).Compare(sorted[j]) == types.IntNegOne
})

return types.DefaultTypeAdapter.NativeToValue(sorted), 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 {
overloads[i] = template(t)
}
return overloads
}
3 changes: 3 additions & 0 deletions ext/lists_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ 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: `[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"},
}

env := testListsEnv(t)
Expand Down

0 comments on commit 45bd6f6

Please sign in to comment.