From f893e8834a0a49977da2dc8d882b1b9db2b47209 Mon Sep 17 00:00:00 2001 From: Vivek Patel Date: Wed, 29 Dec 2021 14:31:05 +0530 Subject: [PATCH] Add expr function for sorting an array It allows ascending/descending sorting along with sorting of native and non-native types --- pkg/expr/func_arr.go | 76 +++++++++++++ pkg/expr/func_arr_test.go | 222 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) diff --git a/pkg/expr/func_arr.go b/pkg/expr/func_arr.go index 1a07ab4f9..2a778fcfb 100644 --- a/pkg/expr/func_arr.go +++ b/pkg/expr/func_arr.go @@ -3,6 +3,7 @@ package expr import ( "fmt" "reflect" + "sort" "strings" "github.com/PaesslerAG/gval" @@ -17,6 +18,7 @@ func ArrayFunctions() []gval.Language { gval.Function("has", has), gval.Function("hasAll", hasAll), gval.Function("find", find), + gval.Function("sort", sortSlice), } } @@ -220,3 +222,77 @@ func slice(arr interface{}, start, end int) interface{} { return v.Slice(start, end).Interface() } + +// sortSlice sorts slice +func sortSlice(arr interface{}, desc bool) (out interface{}, err error) { + if arr, err = toSlice(arr); err != nil { + return + } + + // sort slice of native type values + var ( + c = reflect.ValueOf(arr) + v = reflect.MakeSlice(c.Type(), c.Len(), c.Cap()) + vi = v.Interface() + ) + + reflect.Copy(v, c) + + // sortStable sorts the slice x using the provided less + // function, keeping equal elements in their original order + // + // sort the array if error is nil; + // we trap the error(s) in the outer scope from the less function, + // and returning the last error at a time since all error will the same in almost every scenario. + sort.SliceStable(vi, func(i, j int) bool { + if err != nil { + return false + } + + var ( + nVal = v.Index(i) + mVal = v.Index(j) + ) + if desc { + nVal = v.Index(j) + mVal = v.Index(i) + } + + switch getKind(vi) { + case reflect.String: + return nVal.String() < mVal.String() + case reflect.Int, reflect.Int32, reflect.Int64: + return nVal.Int() < mVal.Int() + case reflect.Float32, reflect.Float64: + return nVal.Float() < mVal.Float() + default: + // sort slice of typed values + if stv, is := vi.([]TypedValue); is { + if casted, ok := stv[i].(Comparable); ok { + var cmp int + cmp, err = casted.Compare(stv[j]) + if err != nil { + return false + } + if desc { + return cmp > 0 + } else { + return cmp < 0 + } + } else { + err = fmt.Errorf("cannot compare %s and %s: unknown state", stv[i].Type(), stv[j].Type()) + return false + } + } + err = fmt.Errorf("cannot compare %s and %s: unknown state", getKind(nVal), getKind(mVal)) + return false + } + }) + + // returns error from less function + if err != nil { + return arr, err + } + + return vi, nil +} diff --git a/pkg/expr/func_arr_test.go b/pkg/expr/func_arr_test.go index dd8fe0371..255c377cc 100644 --- a/pkg/expr/func_arr_test.go +++ b/pkg/expr/func_arr_test.go @@ -12,12 +12,14 @@ var ( stringArr = []string{"first"} boolArr = []bool{true, true, false} floatArr = []float64{69.420} + strArr = []string{"5", "3", "1", "2"} vals = map[string]interface{}{ "intArr": intArr, "stringArr": stringArr, "boolArr": boolArr, "floatArr": floatArr, + "strArr": strArr, "intVal": 42, "stringVal": "foobar", "boolVal": false, @@ -73,6 +75,20 @@ func Example_pop_float() { // 69.42 } +func Example_sort_string_asc() { + eval(`sort(strArr, false)`, vals) + + // output: + // [1 2 3 5] +} + +func Example_sort_string_desc() { + eval(`sort(strArr, true)`, vals) + + // output: + // [5 3 2 1] +} + func Test_push(t *testing.T) { tcc := []struct { base interface{} @@ -476,6 +492,212 @@ func Test_slice(t *testing.T) { } } +func Test_sortSlice(t *testing.T) { + var ( + s1 = Must(NewString("1")) + s2 = Must(NewString("2")) + s3 = Must(NewString("3")) + s5 = Must(NewString("5")) + + f1 = Must(NewFloat(11.1)) + f2 = Must(NewString(22.2)) + f3 = Must(NewString(33.3)) + f5 = Must(NewString(55.5)) + + a1 = Must(NewAny("1")) + a2 = Must(NewAny("2")) + a3 = Must(NewAny("3")) + a5 = Must(NewAny("5")) + + tcc = []struct { + name string + desc bool + arr interface{} + cloneArr interface{} + expect interface{} + expectErr error + }{ + { + name: "ascending sorting for string array", + arr: []string{"3", "1", "2", "5"}, + cloneArr: []string{"3", "1", "2", "5"}, + expect: []string{"1", "2", "3", "5"}, + }, + { + name: "ascending sorting for string array with multiple identical element", + arr: []string{"1", "3", "2", "2"}, + cloneArr: []string{"1", "3", "2", "2"}, + expect: []string{"1", "2", "2", "3"}, + }, + { + name: "descending sorting for string array", + desc: true, + arr: []string{"3", "1", "2", "5"}, + cloneArr: []string{"3", "1", "2", "5"}, + expect: []string{"5", "3", "2", "1"}, + }, + { + name: "ascending sorting for int array", + arr: []int{1, 3, 5, 2}, + cloneArr: []int{1, 3, 5, 2}, + expect: []int{1, 2, 3, 5}, + }, + { + name: "descending sorting for int array", + desc: true, + arr: []int{1, 3, 5, 2}, + cloneArr: []int{1, 3, 5, 2}, + expect: []int{5, 3, 2, 1}, + }, + { + name: "ascending sorting for float32 array", + arr: []float32{11.1, 33.3, 55.5, 22.2}, + cloneArr: []float32{11.1, 33.3, 55.5, 22.2}, + expect: []float32{11.1, 22.2, 33.3, 55.5}, + }, + { + name: "descending sorting for float32 array", + desc: true, + arr: []float32{11.1, 33.3, 55.5, 22.2}, + cloneArr: []float32{11.1, 33.3, 55.5, 22.2}, + expect: []float32{55.5, 33.3, 22.2, 11.1}, + }, + { + name: "ascending sorting for float64 array", + arr: []float64{11.1, 33.3, 55.5, 22.2}, + cloneArr: []float64{11.1, 33.3, 55.5, 22.2}, + expect: []float64{11.1, 22.2, 33.3, 55.5}, + }, + { + name: "descending sorting for float64 array", + desc: true, + arr: []float64{11.1, 33.3, 55.5, 22.2}, + cloneArr: []float64{11.1, 33.3, 55.5, 22.2}, + expect: []float64{55.5, 33.3, 22.2, 11.1}, + }, + { + name: "ascending sorting for typedValue array of string", + arr: []TypedValue{ + s5, + s3, + s2, + s1, + }, + cloneArr: []TypedValue{ + s5, + s3, + s2, + s1, + }, + expect: []TypedValue{ + s1, + s2, + s3, + s5, + }, + }, + { + name: "descending sorting for typedValue array of string", + desc: true, + arr: []TypedValue{ + s3, + s1, + s2, + s5, + }, + cloneArr: []TypedValue{ + s3, + s1, + s2, + s5, + }, + expect: []TypedValue{ + s5, + s3, + s2, + s1, + }, + }, + { + name: "ascending sorting for typedValue array of float", + arr: []TypedValue{ + f5, + f3, + f2, + f1, + }, + cloneArr: []TypedValue{ + f5, + f3, + f2, + f1, + }, + expect: []TypedValue{ + f1, + f2, + f3, + f5, + }, + }, + { + name: "descending sorting for typedValue array of float", + desc: true, + arr: []TypedValue{ + f3, + f1, + f2, + f5, + }, + cloneArr: []TypedValue{ + f3, + f1, + f2, + f5, + }, + expect: []TypedValue{ + f5, + f3, + f2, + f1, + }, + }, + { + name: "expect error due to sorting for typedValue array of Any(Not Comparable)", + arr: []TypedValue{ + a5, + a3, + a2, + a1, + }, + cloneArr: []TypedValue{ + a5, + a3, + a2, + a1, + }, + expectErr: fmt.Errorf("cannot compare Any and Any: unknown state"), + }, + } + ) + + for _, tc := range tcc { + t.Run(tc.name, func(t *testing.T) { + var ( + req = require.New(t) + ) + + ss, err := sortSlice(tc.arr, tc.desc) + if tc.expectErr != nil { + req.Equal(tc.expectErr, err) + return + } + req.NoError(err) + req.Equal(tc.expect, ss) + req.Equal(tc.cloneArr, tc.arr) + }) + } +} + func must(v []TypedValue, err error) []TypedValue { if err != nil { panic(err)