From 885830428b3804e83cebe07214ea91441ce4fd8a Mon Sep 17 00:00:00 2001 From: Anders Xiao Date: Tue, 2 Mar 2021 14:34:04 +0800 Subject: [PATCH] PR: builtin range function #326 (#328) * builtin range function #326 * change empty range logic * fix unit test error message * fix github env (#329) * fix ErrInvalidRangeStep comments * fix github env (#329) * builtin range function #326 * change empty range logic * fix unit test error message * fix ErrInvalidRangeStep comments * fix lint Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- builtins.go | 69 +++++++++++++++++++++ builtins_test.go | 153 +++++++++++++++++++++++++++++++++++++++++++++++ errors.go | 3 + 3 files changed, 225 insertions(+) diff --git a/builtins.go b/builtins.go index fcda81cd..b954d072 100644 --- a/builtins.go +++ b/builtins.go @@ -121,6 +121,10 @@ var builtinFuncs = []*BuiltinFunction{ Name: "format", Value: builtinFormat, }, + { + Name: "range", + Value: builtinRange, + }, } // GetAllBuiltinFunctions returns all builtin function objects. @@ -323,6 +327,71 @@ func builtinLen(args ...Object) (Object, error) { } } +//range(start, stop[, step]) +func builtinRange(args ...Object) (Object, error) { + numArgs := len(args) + if numArgs < 2 || numArgs > 3 { + return nil, ErrWrongNumArguments + } + var start, stop, step *Int + + for i, arg := range args { + v, ok := args[i].(*Int) + if !ok { + var name string + switch i { + case 0: + name = "start" + case 1: + name = "stop" + case 2: + name = "step" + } + + return nil, ErrInvalidArgumentType{ + Name: name, + Expected: "int", + Found: arg.TypeName(), + } + } + if i == 2 && v.Value <= 0 { + return nil, ErrInvalidRangeStep + } + switch i { + case 0: + start = v + case 1: + stop = v + case 2: + step = v + } + } + + if step == nil { + step = &Int{Value: int64(1)} + } + + return buildRange(start.Value, stop.Value, step.Value), nil +} + +func buildRange(start, stop, step int64) *Array { + array := &Array{} + if start <= stop { + for i := start; i < stop; i += step { + array.Value = append(array.Value, &Int{ + Value: i, + }) + } + } else { + for i := start; i > stop; i -= step { + array.Value = append(array.Value, &Int{ + Value: i, + }) + } + } + return array +} + func builtinFormat(args ...Object) (Object, error) { numArgs := len(args) if numArgs == 0 { diff --git a/builtins_test.go b/builtins_test.go index 2f2e47c2..eca2d5bb 100644 --- a/builtins_test.go +++ b/builtins_test.go @@ -351,3 +351,156 @@ func Test_builtinSplice(t *testing.T) { }) } } + +func Test_builtinRange(t *testing.T) { + var builtinRange func(args ...tengo.Object) (tengo.Object, error) + for _, f := range tengo.GetAllBuiltinFunctions() { + if f.Name == "range" { + builtinRange = f.Value + break + } + } + if builtinRange == nil { + t.Fatal("builtin range not found") + } + tests := []struct { + name string + args []tengo.Object + result *tengo.Array + wantErr bool + wantedErr error + }{ + {name: "no args", args: []tengo.Object{}, wantErr: true, + wantedErr: tengo.ErrWrongNumArguments, + }, + {name: "single args", args: []tengo.Object{&tengo.Map{}}, + wantErr: true, + wantedErr: tengo.ErrWrongNumArguments, + }, + {name: "4 args", args: []tengo.Object{&tengo.Map{}, &tengo.String{}, &tengo.String{}, &tengo.String{}}, + wantErr: true, + wantedErr: tengo.ErrWrongNumArguments, + }, + {name: "invalid start", + args: []tengo.Object{&tengo.String{}, &tengo.String{}}, + wantErr: true, + wantedErr: tengo.ErrInvalidArgumentType{ + Name: "start", Expected: "int", Found: "string"}, + }, + {name: "invalid stop", + args: []tengo.Object{&tengo.Int{}, &tengo.String{}}, + wantErr: true, + wantedErr: tengo.ErrInvalidArgumentType{ + Name: "stop", Expected: "int", Found: "string"}, + }, + {name: "invalid step", + args: []tengo.Object{&tengo.Int{}, &tengo.Int{}, &tengo.String{}}, + wantErr: true, + wantedErr: tengo.ErrInvalidArgumentType{ + Name: "step", Expected: "int", Found: "string"}, + }, + {name: "zero step", + args: []tengo.Object{&tengo.Int{}, &tengo.Int{}, &tengo.Int{}}, //must greate than 0 + wantErr: true, + wantedErr: tengo.ErrInvalidRangeStep, + }, + {name: "negative step", + args: []tengo.Object{&tengo.Int{}, &tengo.Int{}, intObject(-2)}, //must greate than 0 + wantErr: true, + wantedErr: tengo.ErrInvalidRangeStep, + }, + {name: "same bound", + args: []tengo.Object{&tengo.Int{}, &tengo.Int{}}, + wantErr: false, + result: &tengo.Array{ + Value: nil, + }, + }, + {name: "positive range", + args: []tengo.Object{&tengo.Int{}, &tengo.Int{Value: 5}}, + wantErr: false, + result: &tengo.Array{ + Value: []tengo.Object{ + intObject(0), + intObject(1), + intObject(2), + intObject(3), + intObject(4), + }, + }, + }, + {name: "negative range", + args: []tengo.Object{&tengo.Int{}, &tengo.Int{Value: -5}}, + wantErr: false, + result: &tengo.Array{ + Value: []tengo.Object{ + intObject(0), + intObject(-1), + intObject(-2), + intObject(-3), + intObject(-4), + }, + }, + }, + + {name: "positive with step", + args: []tengo.Object{&tengo.Int{}, &tengo.Int{Value: 5}, &tengo.Int{Value: 2}}, + wantErr: false, + result: &tengo.Array{ + Value: []tengo.Object{ + intObject(0), + intObject(2), + intObject(4), + }, + }, + }, + + {name: "negative with step", + args: []tengo.Object{&tengo.Int{}, &tengo.Int{Value: -10}, &tengo.Int{Value: 2}}, + wantErr: false, + result: &tengo.Array{ + Value: []tengo.Object{ + intObject(0), + intObject(-2), + intObject(-4), + intObject(-6), + intObject(-8), + }, + }, + }, + + {name: "large range", + args: []tengo.Object{intObject(-10), intObject(10), &tengo.Int{Value: 3}}, + wantErr: false, + result: &tengo.Array{ + Value: []tengo.Object{ + intObject(-10), + intObject(-7), + intObject(-4), + intObject(-1), + intObject(2), + intObject(5), + intObject(8), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := builtinRange(tt.args...) + if (err != nil) != tt.wantErr { + t.Errorf("builtinRange() error = %v, wantErr %v", + err, tt.wantErr) + return + } + if tt.wantErr && tt.wantedErr.Error() != err.Error() { + t.Errorf("builtinRange() error = %v, wantedErr %v", + err, tt.wantedErr) + } + if tt.result != nil && !reflect.DeepEqual(tt.result, got) { + t.Errorf("builtinRange() arrays are not equal expected"+ + " %s, got %s", tt.result, got.(*tengo.Array)) + } + }) + } +} diff --git a/errors.go b/errors.go index a3fd1f3b..8ef610a3 100644 --- a/errors.go +++ b/errors.go @@ -49,6 +49,9 @@ var ( // ErrNotImplemented is an error where an Object has not implemented a // required method. ErrNotImplemented = errors.New("not implemented") + + // ErrInvalidRangeStep is an error where the step parameter is less than or equal to 0 when using builtin range function. + ErrInvalidRangeStep = errors.New("range step must be greater than 0") ) // ErrInvalidArgumentType represents an invalid argument value type error.