Pārlūkot izejas kodu

feat(func): add `convert_tz` for timezone conversion (#2069)

* feat: add `convert_tz` for timezone conversion

Fixes:

- #986
- #2065

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>

* add convert test cases

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>

* add error test cases

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>

* add validation test cases

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>

---------

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>
Jason Lyu 1 gadu atpakaļ
vecāks
revīzija
aa7ebe3d3c

+ 8 - 0
docs/en_US/sqls/functions/transform_functions.md

@@ -21,6 +21,14 @@ When casting to a datetime type, the supported column type and casting rule are:
 3. If column is string, it will try to automatically detect the format and convert it to datetime type.
 4. Other types are not supported.
 
+## CONVERT_TZ
+
+```text
+convert_tz(col, "Asia/Shanghai")
+```
+
+Convert a time value to a time in the corresponding time zone. The time zone parameter format refers to [IANA Time Zone Database](https://www.iana.org/time-zones), the default value is `UTC`. Set to `Local` to use the system time zone.
+
 ## ENCODE
 
 ```text

+ 9 - 1
docs/zh_CN/sqls/functions/transform_functions.md

@@ -8,7 +8,7 @@
 cast(col,  "bigint")
 ```
 
-将值从一种数据类型转换为另一种数据类型。 支持的类型包括:bigint,float,string,boolean,bytea 和 datetime。
+将值从一种数据类型转换为另一种数据类型。支持的类型包括:bigint,float,string,boolean,bytea 和 datetime。
 
 ### 转换为 datetime 类型
 
@@ -19,6 +19,14 @@ cast(col,  "bigint")
 3. 如果参数为 string 类型,则会尝试自动识别格式并将其转换为 datetime类型。
 4. 其他类型的参数均不支持转换。
 
+## CONVERT_TZ
+
+```text
+convert_tz(col, "Asia/Shanghai")
+```
+
+将时间数值转换成对应时区的时间。时区参数格式参照 [IANA 时区数据库](https://www.iana.org/time-zones),默认值为 `UTC`,设置为 `Local` 则使用系统时区。
+
 ## CHR
 
 ```text

+ 28 - 0
internal/binder/function/funcs_misc.go

@@ -63,6 +63,34 @@ func registerMiscFunc() {
 		},
 		check: returnNilIfHasAnyNil,
 	}
+	builtins["convert_tz"] = builtinFunc{
+		fType: ast.FuncTypeScalar,
+		exec: func(ctx api.FunctionContext, args []interface{}) (interface{}, bool) {
+			arg0, err := cast.InterfaceToTime(args[0], "")
+			if err != nil {
+				return err, false
+			}
+			arg1 := cast.ToStringAlways(args[1])
+			loc, err := time.LoadLocation(arg1)
+			if err != nil {
+				return err, false
+			}
+			return arg0.In(loc), true
+		},
+		val: func(_ api.FunctionContext, args []ast.Expr) error {
+			if err := ValidateLen(2, len(args)); err != nil {
+				return err
+			}
+			if ast.IsNumericArg(args[0]) || ast.IsStringArg(args[0]) || ast.IsBooleanArg(args[0]) {
+				return ProduceErrInfo(0, "datetime")
+			}
+			if ast.IsNumericArg(args[1]) || ast.IsTimeArg(args[1]) || ast.IsBooleanArg(args[1]) {
+				return ProduceErrInfo(1, "string")
+			}
+			return nil
+		},
+		check: returnNilIfHasAnyNil,
+	}
 	builtins["to_json"] = builtinFunc{
 		fType: ast.FuncTypeScalar,
 		exec: func(ctx api.FunctionContext, args []interface{}) (interface{}, bool) {

+ 93 - 0
internal/binder/function/funcs_misc_test.go

@@ -15,10 +15,13 @@
 package function
 
 import (
+	"errors"
 	"fmt"
 	"reflect"
 	"testing"
+	"time"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"github.com/lf-edge/ekuiper/internal/conf"
@@ -241,6 +244,96 @@ func TestFromJson(t *testing.T) {
 	}
 }
 
+func TestConvertTZ(t *testing.T) {
+	f, ok := builtins["convert_tz"]
+	if !ok {
+		t.Fatal("builtin not found")
+	}
+	contextLogger := conf.Log.WithField("rule", "testExec")
+	ctx := kctx.WithValue(kctx.Background(), kctx.LoggerKey, contextLogger)
+	tempStore, _ := state.CreateStore("mockRule0", api.AtMostOnce)
+	fctx := kctx.NewDefaultFuncContext(ctx.WithMeta("mockRule0", "test", tempStore), 2)
+
+	loc, _ := time.LoadLocation("Asia/Shanghai")
+
+	tests := []struct {
+		args   []interface{}
+		result interface{}
+	}{
+		{ // 0
+			args: []interface{}{
+				time.Date(2022, time.April, 13, 6, 22, 32, 233000000, time.UTC),
+				"UTC",
+			},
+			result: time.Date(2022, time.April, 13, 6, 22, 32, 233000000, time.UTC),
+		}, { // 1
+			args: []interface{}{
+				time.Date(2022, time.April, 13, 6, 22, 32, 233000000, time.UTC),
+				"Asia/Shanghai",
+			},
+			result: time.Date(2022, time.April, 13, 14, 22, 32, 233000000, loc),
+		}, { // 2
+			args: []interface{}{
+				time.Date(2022, time.April, 13, 6, 22, 32, 233000000, time.UTC),
+				"Unknown",
+			},
+			result: errors.New("unknown time zone Unknown"),
+		}, { // 3
+			args: []interface{}{
+				true,
+				"UTC",
+			},
+			result: errors.New("unsupported type to convert to timestamp true"),
+		},
+	}
+	for _, tt := range tests {
+		result, _ := f.exec(fctx, tt.args)
+		assert.Equal(t, tt.result, result)
+	}
+
+	vtests := []struct {
+		args    []ast.Expr
+		wantErr bool
+	}{
+		{
+			[]ast.Expr{&ast.TimeLiteral{Val: 0}, &ast.StringLiteral{Val: "0"}},
+			false,
+		},
+		{
+			[]ast.Expr{&ast.StringLiteral{Val: "0"}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.NumberLiteral{Val: 0}, &ast.NumberLiteral{Val: 0}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.NumberLiteral{Val: 0}, &ast.TimeLiteral{Val: 0}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.NumberLiteral{Val: 0}, &ast.BooleanLiteral{Val: true}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.StringLiteral{Val: "0"}, &ast.NumberLiteral{Val: 0}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.BooleanLiteral{Val: true}, &ast.NumberLiteral{Val: 0}},
+			true,
+		},
+	}
+	for _, vtt := range vtests {
+		err := f.val(fctx, vtt.args)
+		if vtt.wantErr {
+			assert.Error(t, err)
+		} else {
+			assert.NoError(t, err)
+		}
+	}
+}
+
 func TestDelay(t *testing.T) {
 	f, ok := builtins["delay"]
 	if !ok {