From 8fa2e3efb96eafe19a03727cba6b6734a623fbec Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 26 Jan 2025 09:36:16 +1100 Subject: [PATCH] Added lists:batch and modified and and or to be useful to handling nil defaults --- _docs/mod/list.md | 9 +++++++++ cmd/cmsh/main.go | 1 + ucl/builtins.go | 12 ++++++------ ucl/builtins/lists.go | 35 ++++++++++++++++++++++++++++++++++- ucl/builtins/lists_test.go | 35 +++++++++++++++++++++++++++++++++++ ucl/eval.go | 14 +++++++------- ucl/objs.go | 28 ++++++++++++++-------------- ucl/testbuiltins_test.go | 6 ++++-- 8 files changed, 110 insertions(+), 30 deletions(-) diff --git a/_docs/mod/list.md b/_docs/mod/list.md index edddc09..b4fc1d8 100644 --- a/_docs/mod/list.md +++ b/_docs/mod/list.md @@ -11,3 +11,12 @@ lists:add LIST [ARGS...] Adds values to the end of the list. Returns the modified list. +### batch + +``` +lists:batch LIST SIZE +``` + +Returns a list containing the items of LIST grouped into a sub-list no greater +than SIZE. SIZE must be an integer greater than 0. If LIST is empty, +then an empty list is returned. \ No newline at end of file diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index baaf1aa..f2fd688 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -21,6 +21,7 @@ func main() { ucl.WithModule(builtins.CSV(nil)), ucl.WithModule(builtins.FS(nil)), ucl.WithModule(builtins.Log(nil)), + ucl.WithModule(builtins.Lists()), ucl.WithModule(builtins.OS()), ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Time()), diff --git a/ucl/builtins.go b/ucl/builtins.go index a26ccb4..539d119 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -273,7 +273,7 @@ func andBuiltin(ctx context.Context, args invocationArgs) (Object, error) { for _, a := range args.args { if a == nil || !a.Truthy() { - return boolObject(false), nil + return a, nil } } return args.args[len(args.args)-1], nil @@ -284,12 +284,12 @@ func orBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return nil, err } - for _, a := range args.args { + for _, a := range args.args[:len(args.args)-1] { if a != nil && a.Truthy() { return a, nil } } - return boolObject(false), nil + return args.args[len(args.args)-1], nil } func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) { @@ -510,7 +510,7 @@ func keysBuiltin(ctx context.Context, args invocationArgs) (Object, error) { val := args.args[0] switch v := val.(type) { case hashable: - keys := make(listObject, 0, v.Len()) + keys := make(ListObject, 0, v.Len()) if err := v.Each(func(k string, _ Object) error { keys = append(keys, StringObject(k)) return nil @@ -536,7 +536,7 @@ func mapBuiltin(ctx context.Context, args invocationArgs) (Object, error) { switch t := args.args[0].(type) { case Listable: l := t.Len() - newList := listObject{} + newList := ListObject{} for i := 0; i < l; i++ { v := t.Index(i) m, err := inv.invoke(ctx, args.fork([]Object{v})) @@ -563,7 +563,7 @@ func filterBuiltin(ctx context.Context, args invocationArgs) (Object, error) { switch t := args.args[0].(type) { case Listable: l := t.Len() - newList := listObject{} + newList := ListObject{} for i := 0; i < l; i++ { v := t.Index(i) m, err := inv.invoke(ctx, args.fork([]Object{v})) diff --git a/ucl/builtins/lists.go b/ucl/builtins/lists.go index be3259d..48c4a0f 100644 --- a/ucl/builtins/lists.go +++ b/ucl/builtins/lists.go @@ -2,6 +2,7 @@ package builtins import ( "context" + "errors" "ucl.lmika.dev/ucl" ) @@ -9,7 +10,8 @@ func Lists() ucl.Module { return ucl.Module{ Name: "lists", Builtins: map[string]ucl.BuiltinHandler{ - "add": listAdd, + "add": listAdd, + "batch": listBatch, }, } } @@ -33,3 +35,34 @@ func listAdd(ctx context.Context, args ucl.CallArgs) (any, error) { return target, nil } + +func listBatch(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + src ucl.Listable + batchSize int + ) + + if err := args.Bind(&src, &batchSize); err != nil { + return nil, err + } + + if batchSize < 1 { + return nil, errors.New("batch size must be greater than zero") + } + + res := ucl.NewListObject() + batch := ucl.NewListObject() + for i := 0; i < src.Len(); i++ { + batch.Append(src.Index(i)) + if batch.Len() >= batchSize { + res.Append(batch) + batch = ucl.NewListObject() + } + } + + if batch.Len() > 0 { + res.Append(batch) + } + + return res, nil +} diff --git a/ucl/builtins/lists_test.go b/ucl/builtins/lists_test.go index 8d8b909..f3b1c7a 100644 --- a/ucl/builtins/lists_test.go +++ b/ucl/builtins/lists_test.go @@ -35,3 +35,38 @@ func TestLists_Add(t *testing.T) { }) } } + +func TestLists_Batch(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "list batch 1", eval: `lists:batch [1 2 3] 2`, want: []any{[]any{1, 2}, []any{3}}}, + {desc: "list batch 2", eval: `lists:batch [1 2 3 4] 2`, want: []any{[]any{1, 2}, []any{3, 4}}}, + {desc: "list batch 3", eval: `lists:batch [1 2 3] 3`, want: []any{[]any{1, 2, 3}}}, + {desc: "list batch 4", eval: `lists:batch [1 2 3] 7`, want: []any{[]any{1, 2, 3}}}, + {desc: "list batch 5", eval: `lists:batch [1 2 3] 1`, want: []any{[]any{1}, []any{2}, []any{3}}}, + {desc: "list batch 6", eval: `lists:batch [] 1`, want: []any{}}, + + {desc: "err list batch 1", eval: `lists:batch [1 2 3] 0`, wantErr: true}, + {desc: "err list batch 2", eval: `lists:batch [1 2 3] -3`, wantErr: true}, + {desc: "err list batch 3", eval: `lists:batch [1 2 3] "waht"`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Lists()), + ) + res, err := inst.Eval(context.Background(), tt.eval) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + } + }) + } +} diff --git a/ucl/eval.go b/ucl/eval.go index 506cb12..237864b 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -108,9 +108,9 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object, func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd, cmd invokable) (Object, error) { var ( - pargs listObject - kwargs map[string]*listObject - argsPtr *listObject + pargs ListObject + kwargs map[string]*ListObject + argsPtr *ListObject ) argsPtr = &pargs @@ -121,10 +121,10 @@ func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe O if ident := arg.Arg.Ident; len(arg.DotSuffix) == 0 && ident != nil && ident.String()[0] == '-' { // Arg switch if kwargs == nil { - kwargs = make(map[string]*listObject) + kwargs = make(map[string]*ListObject) } - argsPtr = &listObject{} + argsPtr = &ListObject{} kwargs[ident.String()[1:]] = argsPtr } else { ae, err := e.evalDot(ctx, ec, arg) @@ -203,7 +203,7 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astListOrHash) (Object, error) { if loh.EmptyList { - return &listObject{}, nil + return &ListObject{}, nil } else if loh.EmptyHash { return hashObject{}, nil } @@ -230,7 +230,7 @@ func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astList return h, nil } - l := listObject{} + l := ListObject{} for _, el := range loh.Elements { if el.Right != nil { return nil, errors.New("miss-match of lists and hash") diff --git a/ucl/objs.go b/ucl/objs.go index 934b7f2..8cdd011 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -38,13 +38,17 @@ type hashable interface { Each(func(k string, v Object) error) error } -type listObject []Object +type ListObject []Object -func (lo *listObject) Append(o Object) { +func NewListObject() *ListObject { + return &ListObject{} +} + +func (lo *ListObject) Append(o Object) { *lo = append(*lo, o) } -func (lo *listObject) Insert(idx int, obj Object) error { +func (lo *ListObject) Insert(idx int, obj Object) error { if idx != -1 { return errors.New("not supported") } @@ -52,26 +56,22 @@ func (lo *listObject) Insert(idx int, obj Object) error { return nil } -func (s *listObject) String() string { +func (s *ListObject) String() string { return fmt.Sprintf("%v", []Object(*s)) } -func (s *listObject) Truthy() bool { +func (s *ListObject) Truthy() bool { return len(*s) > 0 } -func (s *listObject) Len() int { +func (s *ListObject) Len() int { return len(*s) } -func (s *listObject) Index(i int) Object { +func (s *ListObject) Index(i int) Object { return (*s)[i] } -func (s *listObject) Add(o Object) { - *s = append(*s, o) -} - type hashObject map[string]Object func (s hashObject) String() string { @@ -171,7 +171,7 @@ func toGoValue(obj Object) (interface{}, bool) { return bool(v), true case timeObject: return time.Time(v), true - case *listObject: + case *ListObject: xs := make([]interface{}, 0, len(*v)) for _, va := range *v { x, ok := toGoValue(va) @@ -357,7 +357,7 @@ type invocationArgs struct { inst *Inst ec *evalCtx args []Object - kwargs map[string]*listObject + kwargs map[string]*ListObject } func (ia invocationArgs) expectArgn(x int) error { @@ -415,7 +415,7 @@ func (ia invocationArgs) fork(args []Object) invocationArgs { inst: ia.inst, ec: ia.ec, args: args, - kwargs: make(map[string]*listObject), + kwargs: make(map[string]*ListObject), } } diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index 3727a77..455680f 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -38,7 +38,7 @@ func WithTestBuiltin() InstOption { })) i.rootEC.addCmd("list", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { - var a listObject = make([]Object, len(args.args)) + var a ListObject = make([]Object, len(args.args)) copy(a, args.args) return &a, nil })) @@ -1498,9 +1498,11 @@ func TestBuiltins_AndOrNot(t *testing.T) { {desc: "not 3", expr: `not $false $true`, want: true}, {desc: "short circuit and 1", expr: `and "hello" "world"`, want: "world"}, - {desc: "short circuit and 2", expr: `and () "world"`, want: false}, + {desc: "short circuit and 2", expr: `and () "world"`, want: nil}, + {desc: "short circuit and 3", expr: `and [] "world"`, want: []any{}}, {desc: "short circuit or 1", expr: `or "hello" "world"`, want: "hello"}, {desc: "short circuit or 2", expr: `or () "world"`, want: "world"}, + {desc: "short circuit or 3", expr: `or () []`, want: []any{}}, {desc: "bad and 1", expr: `and "one"`, wantErr: true}, {desc: "bad and 2", expr: `and`, wantErr: true},