From 16e42c4807bd56a966d201194dd152d1bf4af3c9 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Fri, 11 Aug 2023 09:35:49 -0500 Subject: [PATCH] Add filter expression function --- cmd/docgen/docs/base_test.go | 2 +- excellent/functions/builtin.go | 50 +++++++++++++++++++++++------ excellent/functions/builtin_test.go | 5 +++ excellent/types/function.go | 9 ++++++ excellent/types/function_test.go | 29 +++++++++++++++++ 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/cmd/docgen/docs/base_test.go b/cmd/docgen/docs/base_test.go index e18dbbabc..92218423c 100644 --- a/cmd/docgen/docs/base_test.go +++ b/cmd/docgen/docs/base_test.go @@ -43,7 +43,7 @@ func TestGenerateDocs(t *testing.T) { context := completion["context"].(map[string]interface{}) functions := completion["functions"].([]interface{}) - assert.Equal(t, 87, len(functions)) + assert.Equal(t, 88, len(functions)) types := context["types"].([]interface{}) assert.Equal(t, 18, len(types)) diff --git a/excellent/functions/builtin.go b/excellent/functions/builtin.go index 4d2c0b48e..f57d7316f 100644 --- a/excellent/functions/builtin.go +++ b/excellent/functions/builtin.go @@ -109,6 +109,7 @@ func init() { "sum": OneArrayFunction(Sum), "unique": OneArrayFunction(Unique), "concat": TwoArrayFunction(Concat), + "filter": TwoArgFunction(Filter), // encoded text functions "urn_parts": OneTextFunction(URNParts), @@ -1689,6 +1690,39 @@ func Concat(env envs.Environment, array1 *types.XArray, array2 *types.XArray) ty return types.NewXArray(both...) } +// Filter returns a new array with the items from `array` that when passed to `func` return true. +// +// @(filter(array(1, 0, 2), boolean)) -> [1, 2] +// @(filter(array("a", "b", "c"), (x) => x != "c")) -> [a, b] +// +// @function filter(array, func) +func Filter(env envs.Environment, arg1 types.XValue, arg2 types.XValue) types.XValue { + array, xerr := types.ToXArray(env, arg1) + if xerr != nil { + return xerr + } + function, xerr := types.ToXFunction(arg2) + if xerr != nil { + return xerr + } + + result := make([]types.XValue, 0, array.Count()) + + for i := 0; i < array.Count(); i++ { + item := array.Get(i) + keep := function.Call(env, []types.XValue{item}) + asBool, xerr := types.ToXBoolean(keep) + if xerr != nil { + return xerr + } + if asBool.Native() { + result = append(result, item) + } + } + + return types.NewXArray(result...) +} + //------------------------------------------------------------------------------------------ // Encoded Text Functions //------------------------------------------------------------------------------------------ @@ -2174,14 +2208,12 @@ func ForEach(env envs.Environment, args ...types.XValue) types.XValue { if xerr != nil { return xerr } - - function, isFunction := args[1].(*types.XFunction) - if !isFunction { - return types.NewXErrorf("requires an function as its second argument") + function, xerr := types.ToXFunction(args[1]) + if xerr != nil { + return xerr } otherArgs := args[2:] - result := make([]types.XValue, array.Count()) for i := 0; i < array.Count(); i++ { @@ -2211,14 +2243,12 @@ func ForEachValue(env envs.Environment, args ...types.XValue) types.XValue { if xerr != nil { return xerr } - - function, isFunction := args[1].(*types.XFunction) - if !isFunction { - return types.NewXErrorf("requires an function as its second argument") + function, xerr := types.ToXFunction(args[1]) + if xerr != nil { + return xerr } otherArgs := args[2:] - props := object.Properties() result := make(map[string]types.XValue, len(props)) diff --git a/excellent/functions/builtin_test.go b/excellent/functions/builtin_test.go index f4ba230f4..267e645b4 100644 --- a/excellent/functions/builtin_test.go +++ b/excellent/functions/builtin_test.go @@ -269,6 +269,11 @@ func TestFunctions(t *testing.T) { {"field", dmy, []types.XValue{xs("hello"), xs("1"), ERROR}, ERROR}, {"field", dmy, []types.XValue{}, ERROR}, + {"filter", dmy, []types.XValue{xa(xi(1), xi(0), xi(2)), xf("boolean")}, xa(xi(1), xi(2))}, + {"filter", dmy, []types.XValue{xa(), xf("boolean")}, xa()}, + {"filter", dmy, []types.XValue{ERROR, xf("boolean")}, ERROR}, + {"filter", dmy, []types.XValue{xa(xi(1), xi(0), xi(2)), ERROR}, ERROR}, + {"foreach", dmy, []types.XValue{xa(xs("a"), xs("b"), xs("c")), xf("upper")}, xa(xs("A"), xs("B"), xs("C"))}, {"foreach", dmy, []types.XValue{xa(xs("the man"), xs("fox"), xs("jumped up")), xf("word"), xi(0)}, xa(xs("the"), xs("fox"), xs("jumped"))}, {"foreach", dmy, []types.XValue{ERROR, xf("upper")}, ERROR}, diff --git a/excellent/types/function.go b/excellent/types/function.go index 0b956477b..f23e0195d 100644 --- a/excellent/types/function.go +++ b/excellent/types/function.go @@ -82,3 +82,12 @@ func (x *XFunction) Equals(o XValue) bool { } var _ XValue = (*XFunction)(nil) + +// ToXFunction converts the given value to a function returns an error if that isn't possible +func ToXFunction(x XValue) (*XFunction, XError) { + function, isFunction := x.(*XFunction) + if !isFunction { + return nil, NewXErrorf("%s is not a function", Describe(x)) + } + return function, nil +} diff --git a/excellent/types/function_test.go b/excellent/types/function_test.go index beacfae55..39300994a 100644 --- a/excellent/types/function_test.go +++ b/excellent/types/function_test.go @@ -47,3 +47,32 @@ func TestXFunction(t *testing.T) { assert.False(t, types.Equals(anon1, anon1)) assert.False(t, types.Equals(anon1, anon2)) } + +func TestToXFunction(t *testing.T) { + foo := types.NewXFunction("foo", func(env envs.Environment, args ...types.XValue) types.XValue { return types.NewXText("foo") }) + anon := types.NewXFunction("", func(env envs.Environment, args ...types.XValue) types.XValue { return types.NewXText("bar") }) + + types.ToXFunction(types.NewXText("hello")) + + var tests = []struct { + value types.XValue + asFunc *types.XFunction + hasError bool + }{ + {nil, nil, true}, + {types.NewXText("hi"), nil, true}, + {foo, foo, false}, + {anon, anon, false}, + } + + for _, test := range tests { + fun, err := types.ToXFunction(test.value) + + if test.hasError { + assert.Error(t, err, "expected error for input %T{%s}", test.value, test.value) + } else { + assert.NoError(t, err, "unexpected error for input %T{%s}", test.value, test.value) + assert.Equal(t, test.asFunc, fun, "array mismatch for input %T{%s}", test.value, test.value) + } + } +}