From 20ea8bac060b25072810d3b528731b7620f2f46d Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 4 Sep 2024 22:18:15 +1000 Subject: [PATCH] Added the seq builtin --- ucl/builtins.go | 67 ++++++++++++++++++++++++++++++++++++++++ ucl/inst.go | 1 + ucl/objs.go | 13 ++++++++ ucl/testbuiltins_test.go | 63 +++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+) diff --git a/ucl/builtins.go b/ucl/builtins.go index 2f73f93..e984786 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -361,6 +361,73 @@ func firstBuiltin(ctx context.Context, args invocationArgs) (object, error) { return nil, errors.New("expected listable") } +type seqObject struct { + from int + to int + inclusive bool +} + +func (s seqObject) String() string { + return fmt.Sprintf("%d:%d", s.from, s.to) +} + +func (s seqObject) Truthy() bool { + return s.from != s.to || s.inclusive +} + +func (s seqObject) Len() int { + var l int + if s.from > s.to { + l = s.from - s.to + } else { + l = s.to - s.from + } + if s.inclusive { + l += 1 + } + return l +} + +func (s seqObject) Index(i int) object { + l := s.Len() + if i < 0 || i > l { + return nil + } + if s.from > s.to { + return intObject(s.from - i) + } + return intObject(s.from + i) +} + +func seqBuiltin(ctx context.Context, args invocationArgs) (object, error) { + inclusive := false + if inc, ok := args.kwargs["inc"]; ok { + inclusive = (inc.Len() == 0) || inc.Truthy() + } + + switch len(args.args) { + case 1: + n, err := args.intArg(0) + if err != nil { + return nil, err + } + return seqObject{from: 0, to: n, inclusive: inclusive}, nil + case 2: + f, err := args.intArg(0) + if err != nil { + return nil, err + } + + t, err := args.intArg(1) + if err != nil { + return nil, err + } + return seqObject{from: f, to: t, inclusive: inclusive}, nil + default: + return nil, errors.New("expected either 1 or 2 arguments") + } +} + func ifBuiltin(ctx context.Context, args macroArgs) (object, error) { if args.nargs() < 2 { return nil, errors.New("need at least 2 arguments") diff --git a/ucl/inst.go b/ucl/inst.go index 7fa1f56..a4d1128 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -53,6 +53,7 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("keys", invokableFunc(keysBuiltin)) rootEC.addCmd("index", invokableFunc(indexBuiltin)) rootEC.addCmd("call", invokableFunc(callBuiltin)) + rootEC.addCmd("seq", invokableFunc(seqBuiltin)) rootEC.addCmd("map", invokableFunc(mapBuiltin)) rootEC.addCmd("filter", invokableFunc(filterBuiltin)) diff --git a/ucl/objs.go b/ucl/objs.go index a7765da..f65bcf0 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -298,6 +298,19 @@ func (ia invocationArgs) stringArg(i int) (string, error) { return s.String(), nil } +func (ia invocationArgs) intArg(i int) (int, error) { + if len(ia.args) < i { + return 0, errors.New("expected at least " + strconv.Itoa(i) + " args") + } + + switch v := ia.args[i].(type) { + case intObject: + return int(v), nil + default: + return 0, errors.New("expected an int arg") + } +} + func (ia invocationArgs) invokableArg(i int) (invokable, error) { if len(ia.args) < i { return nil, errors.New("expected at least " + strconv.Itoa(i) + " args") diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index dc80ae6..3ac0e6e 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -461,6 +461,69 @@ func TestBuiltins_Return(t *testing.T) { } } +func TestBuiltins_Seq(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "empty seq", expr: `seq 0 0`, want: ``}, + {desc: "simple seq 1", expr: `seq 5`, want: "0\n1\n2\n3\n4\n"}, + {desc: "simple seq 2", expr: `seq 3`, want: "0\n1\n2\n"}, + {desc: "simple seq 3", expr: `seq -5`, want: "0\n-1\n-2\n-3\n-4\n"}, + {desc: "asc seq 1", expr: `seq 3 5`, want: "3\n4\n"}, + {desc: "asc seq 2", expr: `seq 3 8`, want: "3\n4\n5\n6\n7\n"}, + {desc: "desc seq 1", expr: `seq 8 0`, want: "8\n7\n6\n5\n4\n3\n2\n1\n"}, + {desc: "desc seq 2", expr: `seq 5 2`, want: "5\n4\n3\n"}, + {desc: "desc seq 3", expr: `seq 3 -3`, want: "3\n2\n1\n0\n-1\n-2\n"}, + {desc: "inclusive seq 1", expr: `seq 5 -inc`, want: "0\n1\n2\n3\n4\n5\n"}, + {desc: "inclusive seq 2", expr: `seq -3 -inc`, want: "0\n-1\n-2\n-3\n"}, + {desc: "inclusive seq 3", expr: `seq 5 8 -inc`, want: "5\n6\n7\n8\n"}, + {desc: "inclusive seq 4", expr: `seq 4 0 -inc`, want: "4\n3\n2\n1\n0\n"}, + + {desc: "len of empty seq", expr: `seq 0 0 | len`, want: "0\n"}, + {desc: "len of simple seq 1", expr: `seq 5 | len`, want: "5\n"}, + {desc: "len of simple seq 2", expr: `seq 3 | len`, want: "3\n"}, + {desc: "len of simple seq 3", expr: `seq -5 | len`, want: "5\n"}, + {desc: "len of asc seq 1", expr: `seq 3 5 | len`, want: "2\n"}, + {desc: "len of asc seq 2", expr: `seq 3 8 | len`, want: "5\n"}, + {desc: "len of desc seq 1", expr: `seq 8 0 | len`, want: "8\n"}, + {desc: "len of desc seq 2", expr: `seq 5 2 | len`, want: "3\n"}, + {desc: "len of desc seq 3", expr: `seq 3 -3 | len`, want: "6\n"}, + {desc: "len of inclusive seq 1", expr: `seq 5 -inc | len`, want: "6\n"}, + {desc: "len of inclusive seq 2", expr: `seq -3 -inc | len`, want: "4\n"}, + {desc: "len of inclusive seq 3", expr: `seq 5 8 -inc | len`, want: "4\n"}, + {desc: "len of inclusive seq 4", expr: `seq 4 0 -inc | len`, want: "5\n"}, + + {desc: "truthy of empty seq 1", expr: `if (seq 0 0) { echo "t" }`, want: "(nil)\n"}, + {desc: "truthy of empty seq 2", expr: `if (seq 3 3) { echo "t" }`, want: "(nil)\n"}, + {desc: "truthy of empty seq 3", expr: `if (seq -5 -5) { echo "t" }`, want: "(nil)\n"}, + {desc: "truthy of empty seq 4", expr: `if (seq 0) { echo "t" }`, want: "(nil)\n"}, + {desc: "truthy simple seq", expr: `if (seq 5) { echo "t" }`, want: "t\n(nil)\n"}, + {desc: "truthy asc seq", expr: `if (seq 3 5) { echo "t" }`, want: "t\n(nil)\n"}, + {desc: "truthy desc seq", expr: `if (seq 3 -6) { echo "t" }`, want: "t\n(nil)\n"}, + {desc: "truthy inclusive 1", expr: `if (seq 0 -inc) { echo "t" }`, want: "t\n(nil)\n"}, + {desc: "truthy inclusive 2", expr: `if (seq 0 0 -inc) { echo "t" }`, want: "t\n(nil)\n"}, + {desc: "truthy inclusive 3", expr: `if (seq 3 3 -inc) { echo "t" }`, want: "t\n(nil)\n"}, + + {desc: "map seq", expr: `seq 4 | map { |x| cat "[" $x "]" }`, want: "[0]\n[1]\n[2]\n[3]\n"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + err := EvalAndDisplay(ctx, inst, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } + +} + func TestBuiltins_Map(t *testing.T) { tests := []struct { desc string