Repository: d5/tengo
Pull request #328 (2021-03-02 04:21:50)

PR: builtin range function #326
add builtin range function:   

range(start, stop[, step])   

usage examples: 

range(0,0) ----> []   
range(0, 5) -----> [0, 1, 2, 3, 4]   
range(0, 10, 3) -----> [0, 3, 6, 9]
range(0, -10, 2) -----> [0, -2, -4, -6, -8]

special:

The step must be greater than 0 to prevent infinite loops


geseq: Nice one! 

I would expect range(0,0) to be [] for consistency.
endink: @geseq Okay, i will change range(0,0) to be [] , any ideas for support for a single parameter? 

range(5) to [0, 1, 2, 3, 4], current implementions are not supported, if you want, i can support it
geseq: I think you might be right and I can see one argument being confusing. I pretty much pulled the idea out of python but we don’t need to stick exactly to python syntax.
endink: @geseq  ok, Use the same upper and lower bounds will get empty array ( [] ) as you expect, github action seems to have some problems ? please check this PR. I think it's done now ! 

Patch:
diff --git a/builtins.go b/builtins.go
index fcda81c..b954d07 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/errors.go b/errors.go
index a3fd1f3..8ef610a 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.


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 555f918..72d20d8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -23,8 +23,8 @@ jobs:
           ${{ runner.os }}-go-
     - name: setup env
       run: |
-        echo "::set-env name=GOPATH::$(go env GOPATH)"
-        echo "::add-path::$(go env GOPATH)/bin"
+        echo "name=GOPATH::$(go env GOPATH)" >> $GITHUB_ENV
+        echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
       shell: bash
     - name: check out code
       uses: actions/checkout@v2
diff --git a/builtins_test.go b/builtins_test.go
index 2f2e47c..eca2d5b 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))
+			}
+		})
+	}
+}