diff --git a/_docs/mod/core.md b/_docs/mod/core.md index 60e9020..6b9e3a7 100644 --- a/_docs/mod/core.md +++ b/_docs/mod/core.md @@ -29,6 +29,27 @@ error MSG Raises an error with MSG as the given value. This will start unrolling the stack until a `try` block is encountered, or until it reaches the top level stack, at which it will be displayed as a "runtime error". +### filter + +``` +filter COL BLOCK +``` + +Returns a copy of COL with elements where the predicate BLOCK returns a truthy value. + +### first + +``` +first COL BLOCK +``` + +Returns the first element of COL where the predicate BLOCK returns a truthy value. COL can either be a list or +an iterator. If no value satisfying BLOCK is found, `first` will return nil. + +If COL is a list, `first` will consume from index 0 and will continue until either finding a value that +satisfies the predicate, or until the end of the list is reached. If COL is an iterator, `first` will continue +consuming values until a value satisfying the predicate is found, or until the iterator is exhausted. + ### foreach ``` diff --git a/ucl/builtins.go b/ucl/builtins.go index 3587715..b81eaf6 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -765,7 +765,7 @@ func reduceBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return nil, errors.New("expected listable") } -func firstBuiltin(ctx context.Context, args invocationArgs) (Object, error) { +func headBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err } @@ -785,6 +785,47 @@ func firstBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return nil, errors.New("expected listable") } +func firstBuiltin(ctx context.Context, args invocationArgs) (Object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + inv, err := args.invokableArg(1) + if err != nil { + return nil, err + } + + switch t := args.args[0].(type) { + case Listable: + l := t.Len() + for i := 0; i < l; i++ { + v := t.Index(i) + m, err := inv.invoke(ctx, args.fork([]Object{v})) + if err != nil { + return nil, err + } else if isTruthy(m) { + return v, nil + } + } + return nil, nil + case Iterable: + for t.HasNext() { + v, err := t.Next(ctx) + if err != nil { + return nil, err + } + m, err := inv.invoke(ctx, args.fork([]Object{v})) + if err != nil { + return nil, err + } else if isTruthy(m) { + return v, nil + } + } + return nil, nil + } + return nil, errors.New("expected listable") +} + type seqObject struct { from int to int diff --git a/ucl/inst.go b/ucl/inst.go index d5afbed..1d1fa9a 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -72,7 +72,8 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("map", invokableFunc(mapBuiltin)) rootEC.addCmd("filter", invokableFunc(filterBuiltin)) - rootEC.addCmd("head", invokableFunc(firstBuiltin)) + rootEC.addCmd("first", invokableFunc(firstBuiltin)) + rootEC.addCmd("head", invokableFunc(headBuiltin)) rootEC.addCmd("reduce", invokableFunc(reduceBuiltin)) rootEC.addCmd("eq", invokableFunc(eqBuiltin)) diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index 359b7c9..67c36f5 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -1235,6 +1235,36 @@ func TestBuiltins_Filter(t *testing.T) { } } +func TestBuiltins_First(t *testing.T) { + tests := []struct { + desc string + expr string + want any + }{ + {desc: "first list 1", expr: `first [1 2 3] { |x| eq $x 2 }`, want: 2}, + {desc: "first list 2", expr: `first ["flim" "flam" "fla"] { |x| eq $x "flam" }`, want: "flam"}, + {desc: "first list 3", expr: `first ["flim" "flam" "fla"] { |x| eq $x "bogie" }`, want: nil}, + {desc: "first list 4", expr: `first [() () ()] { |x| $x }`, want: nil}, + {desc: "first list 5", expr: `first [] { |x| $x }`, want: nil}, + + {desc: "first itr 1", expr: `itr | first { |x| ne $x 2 }`, want: 1}, + {desc: "first itr 2", expr: `set t (itr) ; set x (first $t { |x| ne $x 2 }) ; set y (first $t { |x| ne $x 2 }) ; "$x $y"`, want: "1 3"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + + res, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} + func TestBuiltins_Reduce(t *testing.T) { tests := []struct { desc string