作者 yangfu

Merge branch 'test'

... ... @@ -11,6 +11,7 @@ require (
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gavv/httpexpect v2.0.0+incompatible
github.com/go-gota/gota v0.12.0
github.com/go-pg/pg/v10 v10.10.6
github.com/go-redis/redis v6.15.9+incompatible
github.com/google/go-querystring v1.1.0 // indirect
... ... @@ -29,6 +30,7 @@ require (
github.com/stretchr/testify v1.7.1
github.com/valyala/fasthttp v1.38.0 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
github.com/xuri/excelize/v2 v2.6.0
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
github.com/yudai/gojsondiff v1.0.0 // indirect
... ...
package astexpr
import (
"errors"
"fmt"
"strconv"
"strings"
)
type AST struct {
Tokens []*Token
source string
currTok *Token
currIndex int
depth int
Err error
}
func NewAST(toks []*Token, s string) *AST {
a := &AST{
Tokens: toks,
source: s,
}
if a.Tokens == nil || len(a.Tokens) == 0 {
a.Err = errors.New("empty token")
} else {
a.currIndex = 0
a.currTok = a.Tokens[0]
}
return a
}
// ParseExpression 解析表达式
func (a *AST) ParseExpression() ExprAST {
a.depth++ // called depth
lhs := a.parsePrimary()
r := a.parseBinOpRHS(0, lhs)
a.depth--
if a.depth == 0 && a.currIndex != len(a.Tokens) && a.Err == nil {
a.Err = errors.New(
fmt.Sprintf("bad expression, reaching the end or missing the operator\n%s",
ErrPos(a.source, a.currTok.Offset)))
}
return r
}
func (a *AST) getNextToken() *Token {
a.currIndex++
if a.currIndex < len(a.Tokens) {
a.currTok = a.Tokens[a.currIndex]
return a.currTok
}
return nil
}
func (a *AST) getTokPrecedence() int {
if p, ok := precedence[a.currTok.Tok]; ok {
return p
}
return -1
}
func (a *AST) parseNumber() NumberExprAST {
f64, err := strconv.ParseFloat(a.currTok.Tok, 64)
if err != nil {
a.Err = errors.New(
fmt.Sprintf("%v\nwant '(' or '0-9' but get '%s'\n%s",
err.Error(),
a.currTok.Tok,
ErrPos(a.source, a.currTok.Offset)))
return NumberExprAST{}
}
n := NumberExprAST{
Val: f64,
Str: a.currTok.Tok,
}
a.getNextToken()
return n
}
func (a *AST) parseFunCallerOrConst() ExprAST {
name := a.currTok.Tok
a.getNextToken()
// call func
if a.currTok.Tok == "(" {
f := FunCallerExprAST{}
if _, ok := defFunc[strings.ToLower(name)]; !ok {
a.Err = errors.New(
fmt.Sprintf("function `%s` is undefined\n%s",
name,
ErrPos(a.source, a.currTok.Offset)))
return f
}
a.getNextToken()
exprs := make([]ExprAST, 0)
if a.currTok.Tok == ")" {
// function call without parameters
// ignore the process of parameter resolution
} else {
exprs = append(exprs, a.ParseExpression())
for a.currTok.Tok != ")" && a.getNextToken() != nil {
if a.currTok.Type == COMMA {
continue
}
exprs = append(exprs, a.ParseExpression())
}
}
def := defFunc[strings.ToLower(name)]
if def.argc >= 0 && len(exprs) != def.argc {
a.Err = errors.New(
fmt.Sprintf("wrong way calling function `%s`, parameters want %d but get %d\n%s",
name,
def.argc,
len(exprs),
ErrPos(a.source, a.currTok.Offset)))
}
a.getNextToken()
f = NewFunCallerExprAST(name, exprs...)
return f
}
// call const
if v, ok := defConst[name]; ok {
return NumberExprAST{
Val: v,
Str: strconv.FormatFloat(v, 'f', 0, 64),
}
} else {
if strings.Contains(name, ".") {
return NewFieldExprAST(name)
}
a.Err = errors.New(
fmt.Sprintf("const `%s` is undefined\n%s",
name,
ErrPos(a.source, a.currTok.Offset)))
return NumberExprAST{}
}
}
func (a *AST) parsePrimary() ExprAST {
switch a.currTok.Type {
case Identifier:
return a.parseFunCallerOrConst()
case Literal:
return a.parseNumber()
case StringArgs:
e := NewValueExprAST(a.currTok.Tok)
a.getNextToken()
return e
case Operator:
if a.currTok.Tok == "(" {
t := a.getNextToken()
if t == nil {
a.Err = errors.New(
fmt.Sprintf("want '(' or '0-9' but get EOF\n%s",
ErrPos(a.source, a.currTok.Offset)))
return nil
}
e := a.ParseExpression()
if e == nil {
return nil
}
if a.currTok.Tok != ")" {
a.Err = errors.New(
fmt.Sprintf("want ')' but get %s\n%s",
a.currTok.Tok,
ErrPos(a.source, a.currTok.Offset)))
return nil
}
a.getNextToken()
return e
} else if a.currTok.Tok == "-" {
if a.getNextToken() == nil {
a.Err = errors.New(
fmt.Sprintf("want '0-9' but get '-'\n%s",
ErrPos(a.source, a.currTok.Offset)))
return nil
}
bin := NewBinaryExprAST("-", NumberExprAST{}, a.parsePrimary())
return bin
} else {
return a.parseNumber()
}
case COMMA:
a.Err = errors.New(
fmt.Sprintf("want '(' or '0-9' but get %s\n%s",
a.currTok.Tok,
ErrPos(a.source, a.currTok.Offset)))
return nil
default:
return nil
}
}
func (a *AST) parseBinOpRHS(execPrec int, lhs ExprAST) ExprAST {
for {
tokPrec := a.getTokPrecedence()
if tokPrec < execPrec {
return lhs
}
binOp := a.currTok.Tok
if a.getNextToken() == nil {
a.Err = errors.New(
fmt.Sprintf("want '(' or '0-9' but get EOF\n%s",
ErrPos(a.source, a.currTok.Offset)))
return nil
}
rhs := a.parsePrimary()
if rhs == nil {
return nil
}
nextPrec := a.getTokPrecedence()
if tokPrec < nextPrec {
rhs = a.parseBinOpRHS(tokPrec+1, rhs)
if rhs == nil {
return nil
}
}
lhs = NewBinaryExprAST(binOp, lhs, rhs)
}
}
... ...
package astexpr
import (
"errors"
"math"
)
const (
RadianMode = iota
AngleMode
)
type defS struct {
argc int
fun func(expr ...ExprAST) float64
}
// TrigonometricMode enum "RadianMode", "AngleMode"
var TrigonometricMode = RadianMode
var defConst = map[string]float64{
"pi": math.Pi,
}
var defFunc map[string]defS
func init() {
defFunc = map[string]defS{
"sin": {1, defSin},
"cos": {1, defCos},
"tan": {1, defTan},
"cot": {1, defCot},
"sec": {1, defSec},
"csc": {1, defCsc},
"abs": {1, defAbs},
"ceil": {1, defCeil},
"floor": {1, defFloor},
"round": {1, defRound},
"sqrt": {1, defSqrt},
"cbrt": {1, defCbrt},
"noerr": {1, defNoErr},
"max": {-1, defMax},
"min": {-1, defMin},
// excel support
"sum": {-1, defNone},
"if": {-1, defNone},
"sumif": {-1, defNone},
"and": {-1, defNone},
"or": {-1, defNone},
"month": {-1, defNone},
"year": {-1, defNone},
//"round": {-1, defNone},
"rounddown": {-1, defNone},
"roundup": {-1, defNone},
"count": {-1, defNone},
"countifs": {-1, defNone},
//"&": {-1, defNone},
"concat": {-1, defNone},
"sumifs": {-1, defNone},
}
}
// sin(pi/2) = 1
func defSin(expr ...ExprAST) float64 {
return math.Sin(expr2Radian(expr[0]))
}
// cos(0) = 1
func defCos(expr ...ExprAST) float64 {
return math.Cos(expr2Radian(expr[0]))
}
// tan(pi/4) = 1
func defTan(expr ...ExprAST) float64 {
return math.Tan(expr2Radian(expr[0]))
}
// cot(pi/4) = 1
func defCot(expr ...ExprAST) float64 {
return 1 / defTan(expr...)
}
// sec(0) = 1
func defSec(expr ...ExprAST) float64 {
return 1 / defCos(expr...)
}
// csc(pi/2) = 1
func defCsc(expr ...ExprAST) float64 {
return 1 / defSin(expr...)
}
// abs(-2) = 2
func defAbs(expr ...ExprAST) float64 {
return math.Abs(ExprASTResult(expr[0]))
}
// ceil(4.2) = ceil(4.8) = 5
func defCeil(expr ...ExprAST) float64 {
return math.Ceil(ExprASTResult(expr[0]))
}
// floor(4.2) = floor(4.8) = 4
func defFloor(expr ...ExprAST) float64 {
return math.Floor(ExprASTResult(expr[0]))
}
// round(4.2) = 4
// round(4.6) = 5
func defRound(expr ...ExprAST) float64 {
return math.Round(ExprASTResult(expr[0]))
}
// sqrt(4) = 2
// sqrt(4) = abs(sqrt(4))
// returns only the absolute value of the result
func defSqrt(expr ...ExprAST) float64 {
return math.Sqrt(ExprASTResult(expr[0]))
}
// cbrt(27) = 3
func defCbrt(expr ...ExprAST) float64 {
return math.Cbrt(ExprASTResult(expr[0]))
}
// max(2) = 2
// max(2, 3) = 3
// max(2, 3, 1) = 3
func defMax(expr ...ExprAST) float64 {
if len(expr) == 0 {
panic(errors.New("calling function `max` must have at least one parameter."))
}
if len(expr) == 1 {
return ExprASTResult(expr[0])
}
maxV := ExprASTResult(expr[0])
for i := 1; i < len(expr); i++ {
v := ExprASTResult(expr[i])
maxV = math.Max(maxV, v)
}
return maxV
}
// min(2) = 2
// min(2, 3) = 2
// min(2, 3, 1) = 1
func defMin(expr ...ExprAST) float64 {
if len(expr) == 0 {
panic(errors.New("calling function `min` must have at least one parameter."))
}
if len(expr) == 1 {
return ExprASTResult(expr[0])
}
maxV := ExprASTResult(expr[0])
for i := 1; i < len(expr); i++ {
v := ExprASTResult(expr[i])
maxV = math.Min(maxV, v)
}
return maxV
}
// noerr(1/0) = 0
// noerr(2.5/(1-1)) = 0
func defNoErr(expr ...ExprAST) (r float64) {
defer func() {
if e := recover(); e != nil {
r = 0
}
}()
return ExprASTResult(expr[0])
}
func defNone(expr ...ExprAST) (r float64) {
return 0
}
... ...
package domain
package astexpr
import (
"encoding/json"
"fmt"
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/domain"
)
const (
... ... @@ -10,12 +11,19 @@ const (
TypeBinaryExprAST = "BinaryExprAST"
TypeFieldExprAST = "FieldExprAST"
TypeValueExprAST = "ValueExprAST"
TypeNumberExprAST = "NumberExprAST"
)
type ExprAST interface {
toStr() string
}
type NumberExprAST struct {
ExprType string `json:"exprType"`
Val float64
Str string
}
type ValueExprAST struct {
ExprType string `json:"exprType"`
Val string `json:"val"`
... ... @@ -25,7 +33,7 @@ type ValueExprAST struct {
type FieldExprAST struct {
ExprType string `json:"exprType"`
Str string `json:"str"`
Field *TableField `json:"field"`
Field *domain.TableField `json:"field"`
}
type BinaryExprAST struct {
... ... @@ -36,12 +44,19 @@ type BinaryExprAST struct {
}
type FunCallerExprAST struct {
ArrayFlag bool `json:"arrayFlag"`
//ArrayFlag bool `json:"arrayFlag"`
ExprType string `json:"exprType"`
Name string `json:"name"`
Args []ExprAST `json:"args"`
}
func (n NumberExprAST) toStr() string {
return fmt.Sprintf(
"NumberExprAST:%s",
n.Str,
)
}
func (n ValueExprAST) toStr() string {
return fmt.Sprintf(
"ValueExprAST:%s",
... ... @@ -73,7 +88,7 @@ func (n FunCallerExprAST) toStr() string {
}
type CloneFunCallerExprAST struct {
ArrayFlag bool `json:"arrayFlag"`
//ArrayFlag bool `json:"arrayFlag"`
Name string `json:"name"`
Arg []json.RawMessage `json:"args"`
}
... ... @@ -84,7 +99,7 @@ type CloneBinaryExprAST struct {
Rhs json.RawMessage `json:"rhs"`
}
func AstExprUnmarshalJSON(data []byte) (interface{}, error) {
func ExprUnmarshal(data []byte) (interface{}, error) {
var m = make(map[string]interface{})
if err := json.Unmarshal(data, &m); err != nil {
return nil, err
... ... @@ -108,7 +123,7 @@ func unmarshalMapInterface(m map[string]interface{}, rawData []byte) (interface{
}
exprReturn := &FunCallerExprAST{Name: expr.Name, ExprType: TypeFunCallerExprAST}
for i := range expr.Arg {
subExpr, err := AstExprUnmarshalJSON(expr.Arg[i])
subExpr, err := ExprUnmarshal(expr.Arg[i])
if err != nil {
return nil, err
}
... ... @@ -126,7 +141,7 @@ func unmarshalMapInterface(m map[string]interface{}, rawData []byte) (interface{
}
exprReturn := &BinaryExprAST{Op: expr.Op, ExprType: TypeBinaryExprAST}
if len(expr.Lhs) > 0 {
subExpr, err := AstExprUnmarshalJSON(expr.Lhs)
subExpr, err := ExprUnmarshal(expr.Lhs)
if err != nil {
return nil, err
}
... ... @@ -135,7 +150,7 @@ func unmarshalMapInterface(m map[string]interface{}, rawData []byte) (interface{
}
}
if len(expr.Rhs) > 0 {
subExpr, err := AstExprUnmarshalJSON(expr.Rhs)
subExpr, err := ExprUnmarshal(expr.Rhs)
if err != nil {
return nil, err
}
... ...
package astexpr
import (
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/domain"
"strconv"
"strings"
)
func NewNumberExprAST(val string) NumberExprAST {
num, _ := strconv.ParseFloat(val, 64)
return NumberExprAST{
ExprType: TypeNumberExprAST,
Val: num,
Str: val,
}
}
func NewValueExprAST(val string) ValueExprAST {
return ValueExprAST{
ExprType: TypeValueExprAST,
Val: val,
Str: val,
}
}
func NewFieldExprAST(val string) FieldExprAST {
words := strings.Split(val, ".")
table := words[0]
filed := words[0]
if len(words) > 0 {
filed = words[1]
}
return FieldExprAST{
ExprType: TypeFieldExprAST,
Str: val,
Field: &domain.TableField{
FieldName: filed,
FieldSqlName: filed,
TableName: table,
},
}
}
func NewBinaryExprAST(op string, lhs ExprAST, rhs ExprAST) BinaryExprAST {
return BinaryExprAST{
ExprType: TypeBinaryExprAST,
Op: op,
Lhs: lhs,
Rhs: rhs,
}
}
func NewFunCallerExprAST(name string, args ...ExprAST) FunCallerExprAST {
return FunCallerExprAST{
ExprType: TypeFunCallerExprAST,
Name: name,
Args: args,
}
}
... ...
package astexpr
import (
"fmt"
"github.com/go-gota/gota/dataframe"
"github.com/go-gota/gota/series"
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/domain"
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/infrastructure/utils"
"strings"
)
type Calculator struct {
ExprAST ExprAST
DataTable *domain.DataTable
Result []string
}
func NewCalculator(expr string) (*Calculator, error) {
toks, err := ParseToken(expr)
if err != nil {
return nil, err
}
ast := NewAST(toks, expr)
if ast.Err != nil {
return nil, ast.Err
}
ar := ast.ParseExpression()
if ast.Err != nil {
return nil, ast.Err
}
cal := &Calculator{
ExprAST: ar,
}
return cal, nil
}
func (cal *Calculator) SetDataTable(t *domain.DataTable) *Calculator {
cal.DataTable = t
return cal
}
func (cal *Calculator) Exec() error {
return nil
}
func (cal *Calculator) ExprASTResult(ast ExprAST) (*param, error) {
switch ast.(type) {
case BinaryExprAST:
var l, r *param
var err error
ast := ast.(BinaryExprAST)
l, err = cal.ExprASTResult(ast.Lhs)
if err != nil {
return nil, err
}
r, err = cal.ExprASTResult(ast.Rhs)
if err != nil {
return nil, err
}
switch ast.Op {
case "+", "-", "*", "/", "%":
return cal.OpCalc(ast.Op, l, r), nil
default:
}
case NumberExprAST:
f := ast.(NumberExprAST)
return NewResult([]string{f.Str}), nil
case ValueExprAST:
f := ast.(ValueExprAST)
return NewResult([]string{f.Val}), nil
case FieldExprAST:
f := ast.(FieldExprAST)
values := cal.DataTable.Values(&domain.Field{SQLName: f.Field.FieldSqlName})
return NewResult(values), nil
case FunCallerExprAST:
f := ast.(FunCallerExprAST)
//def := defFunc[f.Name]
//def.fun(f.Args...)
args := make([]*param, 0)
for i := range f.Args {
argValue, err := cal.ExprASTResult(f.Args[i])
if err != nil {
return nil, err
}
args = append(args, argValue)
}
return cal.callDef(f.Name, args), nil
}
return nil, nil
}
func (cal *Calculator) callDef(name string, args []*param) *param {
switch strings.ToLower(name) {
case "sum":
return cal.sum(args...)
case "sumifs":
return cal.sumifs(args...)
case "countifs":
return cal.countifs(args...)
}
return cal.sum(args...)
}
func (cal *Calculator) sum(params ...*param) *param {
var res = make([]string, 0)
var total float64
for _, p := range params {
for _, v := range p.data {
total += utils.NewNumberString(v).MustFloat64()
}
}
res = append(res, utils.AssertString(total))
return NewResult(res)
}
func (cal *Calculator) sumifs(params ...*param) *param {
var list = make([]series.Series, 0)
var filters = make([]dataframe.F, 0)
for i := 0; i < len(params)-1; i++ {
col := colName(i)
if i == 0 {
list = append(list, series.New(params[i].Data(), series.Float, col))
continue
}
if i%2 == 1 {
list = append(list, series.New(params[i+1].Data(), series.String, col))
if f, ok := cal.resolverFilter(col, params[i]); ok {
filters = append(filters, f)
}
i++
}
}
df := dataframe.New(list...)
df = df.FilterAggregation(dataframe.And, filters...)
s := df.Col("A0")
return NewResult(s.Records())
}
func (cal *Calculator) countifs(params ...*param) *param {
var list = make([]series.Series, 0)
var filters = make([]dataframe.F, 0)
for i := 0; i < len(params)-1; i++ {
col := colName(i)
if i%2 == 0 {
list = append(list, series.New(params[i].Data(), series.String, col))
if f, ok := cal.resolverFilter(col, params[i+1]); ok {
filters = append(filters, f)
}
i++
}
}
df := dataframe.New(list...)
df = df.FilterAggregation(dataframe.And, filters...)
count := df.Col("A0").Len()
return NewResult([]string{fmt.Sprintf("%d", count)})
}
func (cal *Calculator) resolverFilter(key string, param *param) (dataframe.F, bool) {
if len(param.Data()) == 1 {
condition := param.Data()[0]
tokens, _ := ParseToken(formatTok(condition))
switch tokens[0].Type {
case Operator, CompareOperator:
if tokens[0].Tok == "*" {
return dataframe.F{Colname: key, Comparator: series.CompFunc, Comparando: func(el series.Element) bool {
return strings.Contains(el.String(), strings.Trim(formatTok(condition), "*"))
}}, true
}
return dataframe.F{Colname: key, Comparator: series.Comparator(tokens[0].Tok), Comparando: formatTok(tokens[1].Tok)}, true
case Identifier, Literal, StringArgs:
if tokens[len(tokens)-1].Tok == "*" || tokens[0].Tok == "*" {
return dataframe.F{Colname: key, Comparator: series.CompFunc, Comparando: func(el series.Element) bool {
return strings.Contains(el.String(), strings.Trim(formatTok(condition), "*"))
}}, true
}
return dataframe.F{Colname: key, Comparator: series.Eq, Comparando: formatTok(condition)}, true
}
}
return dataframe.F{}, false
}
func colName(i int) string {
return fmt.Sprintf("A%v", i)
}
func formatTok(tok string) string {
return strings.Trim(tok, `"`)
}
func (cal *Calculator) OpCalc(op string, lp *param, rp *param) *param {
var res = make([]string, 0)
temp := make([]string, 0)
temp = lp.Data()
l := lp.Data()
r := rp.Data()
if lp.Len() < rp.Len() {
l = r
r = temp
}
rIsSingleValue := len(r) == 1
var rValue string
if rIsSingleValue {
rValue = r[0]
}
for i, lValue := range l {
if rIsSingleValue {
res = append(res, opCalc(op, lValue, rValue))
continue
}
if i >= len(r) {
break
}
res = append(res, opCalc(op, lValue, r[i]))
}
return NewResult(res)
}
func opCalc(op, v1, v2 string) string {
fv1 := utils.NumberString(v1).MustFloat64()
fv2 := utils.NumberString(v2).MustFloat64()
switch op {
case "+":
return utils.AssertString(fv1 + fv2)
case "-":
return utils.AssertString(fv1 - fv2)
case "*":
return utils.AssertString(fv1 * fv2)
case "/":
return utils.AssertString(fv1 / fv2)
}
return ""
}
type param struct {
data []string
}
func (p *param) Len() int {
return len(p.data)
}
func (p *param) Data() []string {
return p.data
}
func NewResult(data []string) *param {
return &param{
data: data,
}
}
... ...
package astexpr
import (
"github.com/stretchr/testify/assert"
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/domain"
"testing"
)
var table = &domain.DataTable{
Fields: []*domain.Field{
{
Name: "业绩",
SQLName: "业绩",
},
{
Name: "绩效",
SQLName: "绩效",
},
{
Name: "月份",
SQLName: "月份",
},
},
Data: [][]string{
{
"10000", "0.90", "2月",
},
{
"20000", "0.95", "3月",
},
{
"20000", "0.95", "3月",
},
},
}
var table1 = &domain.DataTable{
Fields: []*domain.Field{
{
Name: "产品",
SQLName: "产品",
},
{
Name: "季度",
SQLName: "季度",
},
{
Name: "数量",
SQLName: "数量",
},
},
Data: [][]string{
{
"苹果", "第一季度", "800",
},
{
"香蕉", "第一季度", "906",
},
{
"西瓜", "第一季度", "968",
},
{
"菠萝", "第一季度", "227",
},
{
"芒果", "第一季度", "612",
},
{
"苹果", "第二季度", "530",
},
{
"香蕉", "第二季度", "950",
},
{
"西瓜", "第二季度", "533",
},
{
"菠萝", "第二季度", "642",
},
{
"芒果", "第二季度", "489",
},
},
}
func TestSumCalculator(t *testing.T) {
inputs := []struct {
expr string
want []string
}{
{
expr: `sum(1000+销售明细.业绩*销售明细.绩效)`,
want: []string{"50000"},
},
{
expr: `sum(销售明细.业绩)`,
want: []string{"50000"},
},
{
expr: `sum(销售明细.业绩/10)`,
want: []string{"5000"},
},
{
expr: `sum(10000,20000,20000)`,
want: []string{"50000"},
},
}
for _, expr := range inputs {
calc, err := NewCalculator(expr.expr)
if err != nil {
t.Fatal(err)
}
calc.SetDataTable(table)
got, err := calc.ExprASTResult(calc.ExprAST)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expr.want, got.data)
}
}
func TestSumIfCalculator(t *testing.T) {
inputs := []struct {
expr string
want []string
}{
{
expr: `sum(sumifs(销售明细.业绩,"3月",销售明细.月份))`,
want: []string{"40000"},
},
{
expr: `sum(sumifs(销售明细.业绩,"3月",销售明细.月份,"<25000",销售明细.业绩))`,
want: []string{"40000"},
},
{
expr: `sum(sumifs(销售明细.业绩,"3*",销售明细.月份))`,
want: []string{"40000"},
},
{
expr: `sum(sumifs(销售明细.业绩,"*月",销售明细.月份))`,
want: []string{"50000"},
},
}
for _, expr := range inputs {
calc, err := NewCalculator(expr.expr)
if err != nil {
t.Fatal(err)
}
calc.SetDataTable(table)
got, err := calc.ExprASTResult(calc.ExprAST)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expr.want, got.data)
}
}
func TestCountIfCalculator(t *testing.T) {
inputs := []struct {
expr string
want []string
}{
{
expr: `countifs(水果季度.产品,"苹果")`,
want: []string{"2"},
},
{
expr: `countifs(水果季度.产品,"*果")`,
want: []string{"4"},
},
{
expr: `countifs(水果季度.季度,"第一季度",水果季度.数量,">600")`,
want: []string{"4"},
},
{
expr: `countifs(水果季度.产品,"*果",水果季度.数量,">600")`,
want: []string{"2"},
},
}
for _, expr := range inputs {
calc, err := NewCalculator(expr.expr)
if err != nil {
t.Fatal(err)
}
calc.SetDataTable(table1)
got, err := calc.ExprASTResult(calc.ExprAST)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expr.want, got.data)
}
}
... ...
package domain
package astexpr
import (
"fmt"
"testing"
"github.com/linmadan/egglib-go/utils/json"
"github.com/stretchr/testify/assert"
"testing"
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/domain"
)
func TestAstExprUnmarshalJSON(t *testing.T) {
... ... @@ -57,7 +60,7 @@ func TestAstExprUnmarshalJSON(t *testing.T) {
&FieldExprAST{
ExprType: TypeFieldExprAST,
Str: "业绩1",
Field: &TableField{
Field: &domain.TableField{
TableId: 1,
TableName: "测试ABC",
TableSqlName: "table_abc_test",
... ... @@ -83,7 +86,7 @@ func TestAstExprUnmarshalJSON(t *testing.T) {
if err != nil {
t.Fatal(err)
}
v, err := AstExprUnmarshalJSON(data)
v, err := ExprUnmarshal(data)
if err != nil {
t.Fatal(err)
}
... ... @@ -93,3 +96,45 @@ func TestAstExprUnmarshalJSON(t *testing.T) {
assert.Equal(t, input.data, v)
}
}
func TestAstExprParse(t *testing.T) {
funs := []struct {
Name string
Exp []string
Debug bool
}{
{
"COUNTIF 多级嵌套",
[]string{
`COUNTIF(销售明细.业绩,"<=1000")-COUNTIF(销售明细.业绩,"<=100")`,
`SUM(1/(COUNTIF(销售明细.业绩,"<=1000")-COUNTIF(销售明细.业绩,"<=100")))`,
},
true,
},
{
"COUNTIF 多级嵌套",
[]string{
`COUNTIF(销售明细.业绩,"<=1000")-COUNTIF(销售明细.业绩,"<=100")`,
`SUM(COUNTIF(销售明细.业绩,"<=1000")-COUNTIF(销售明细.业绩,"<=100"))`,
},
false,
},
}
for _, f := range funs {
if !f.Debug {
continue
}
fmt.Println("测试项目", f.Name)
fmt.Println()
for _, exp := range f.Exp {
fmt.Println("表达式:", exp)
r, err := Parse(exp)
if err != nil {
t.Error(err)
}
if r != 0 {
}
}
}
}
... ...
package astexpr
import (
"errors"
"fmt"
"strings"
)
var precedence = map[string]int{
"+": 20, "-": 20, "*": 40, "/": 40, "%": 40, "^": 60,
"=": 10, ">": 10, "<": 10, "<=": 10, ">=": 10, "&": 40,
}
const (
// Identifier 标识符 e.g.函数名、表字段
Identifier = iota
// Literal 文字 e.g. 50
Literal
// Operator 计算操作 e.g. + - * /
Operator
// COMMA 命令, e.g. (
COMMA
// CompareOperator 比较操作 e.g. < = >
CompareOperator
// StringArgs 字符串参数
StringArgs
)
type Token struct {
// raw characters
Tok string
// type with Literal/Operator
Type,
Flag int
Offset int
}
type Parser struct {
Source string
SourceRunes []rune
ch rune
offset int
err error
}
func ParseToken(s string) ([]*Token, error) {
p := &Parser{
Source: s,
SourceRunes: []rune(s),
err: nil,
//ch: s[0],
}
p.ch = p.SourceRunes[0]
toks := p.parse()
if p.err != nil {
return nil, p.err
}
return toks, nil
}
func (p *Parser) parse() []*Token {
toks := make([]*Token, 0)
for {
tok := p.nextTok()
if tok == nil {
break
}
toks = append(toks, tok)
}
return toks
}
func (p *Parser) nextTok() *Token {
if p.offset >= len(p.SourceRunes) || p.err != nil {
return nil
}
var err error
for p.isWhitespace(p.ch) && err == nil {
err = p.nextCh()
}
start := p.offset
var tok *Token
switch p.ch {
case
'(',
')',
'+',
'-',
'*',
'/',
'^',
'%',
'&':
tok = &Token{
Tok: string(p.ch),
Type: Operator,
}
tok.Offset = start
err = p.nextCh()
case
'>',
'<',
'=':
if p.isCompareWordChar(p.ch) {
for p.isCompareWordChar(p.ch) && p.nextCh() == nil {
}
tok = &Token{
Tok: string(p.SourceRunes[start:p.offset]),
Type: CompareOperator,
}
tok.Offset = start
} else if p.ch != ' ' {
s := fmt.Sprintf("symbol error: unknown '%v', pos [%v:]\n%s",
string(p.ch),
start,
ErrPos(p.Source, start))
p.err = errors.New(s)
}
case
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9':
for p.isDigitNum(p.ch) && p.nextCh() == nil {
if (p.ch == '-' || p.ch == '+') && p.SourceRunes[p.offset-1] != 'e' {
break
}
}
tok = &Token{
Tok: strings.ReplaceAll(string(p.SourceRunes[start:p.offset]), "_", ""),
Type: Literal,
}
tok.Offset = start
case '"':
for (p.isDigitNum(p.ch) || p.isChar(p.ch) || p.isCompareWordChar(p.ch) || p.ch == '*') && p.nextCh() == nil {
if p.ch == '"' {
break
}
}
err = p.nextCh()
tok = &Token{
Tok: string(p.SourceRunes[start:p.offset]),
Type: StringArgs,
}
tok.Offset = start
case ',':
tok = &Token{
Tok: string(p.ch),
Type: COMMA,
}
tok.Offset = start
err = p.nextCh()
default:
if p.isChar(p.ch) {
for p.isWordChar(p.ch) && p.nextCh() == nil {
}
tok = &Token{
Tok: string(p.SourceRunes[start:p.offset]),
Type: Identifier,
}
tok.Offset = start
} else if p.ch != ' ' {
s := fmt.Sprintf("symbol error: unknown '%v', pos [%v:]\n%s",
string(p.ch),
start,
ErrPos(p.Source, start))
p.err = errors.New(s)
}
}
return tok
}
func (p *Parser) nextCh() error {
p.offset++
if p.offset < len(p.SourceRunes) {
p.ch = p.SourceRunes[p.offset]
return nil
}
return errors.New("EOF")
}
func (p *Parser) isWhitespace(c rune) bool {
return c == ' ' ||
c == '\t' ||
c == '\n' ||
c == '\v' ||
c == '\f' ||
c == '\r'
}
func (p *Parser) isDigitNum(c rune) bool {
return '0' <= c && c <= '9' || c == '.' || c == '_' || c == 'e' || c == '-' || c == '+'
}
func (p *Parser) isChar(c rune) bool {
return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '.' || c == '"' || isChineseCharacter(c)
//判断是汉字
}
func (p *Parser) isWordChar(c rune) bool {
return p.isChar(c) || '0' <= c && c <= '9'
}
func (p *Parser) isCompareWordChar(c rune) bool {
return c == '=' || c == '<' || c == '>'
}
func isChineseCharacter(c rune) bool {
return len([]byte(string(c))) > 2
}
... ...
package astexpr
import (
"bytes"
"encoding/json"
"fmt"
"testing"
)
func TestExecA(t *testing.T) {
exp := "1+2"
exec(exp)
}
func TestExecB(t *testing.T) {
exp := "1+2-4"
exec(exp)
}
func TestExecC(t *testing.T) {
exp := "1+2-4*3-8"
exec(exp)
}
func TestExecD(t *testing.T) {
exp := "1+2-(4*3-8)"
exec(exp)
}
func TestExecE(t *testing.T) {
exp := "1+2-(4*3+(1-8))"
exec(exp)
}
func TestExecF(t *testing.T) {
exp := "1+(2-(4*3+(1-8)))"
exec(exp)
}
func TestExecG(t *testing.T) {
exp := "((1-2)*(3-8))*((((9+2222))))"
exec(exp)
}
func TestExecH(t *testing.T) {
exp := "0.8888-0.1 * 444 -0.2"
exec(exp)
}
func TestExecI(t *testing.T) {
exp := "0.8888-0.1 * (444 -0.2)"
exec(exp)
}
func TestExecJ(t *testing.T) {
exp := "1_234_567*2-3"
exec(exp)
}
func TestExecK(t *testing.T) {
exp := "2.3e4*4/3"
exec(exp)
}
func TestExecL(t *testing.T) {
exp := "-1+9-88"
exec(exp)
}
func TestExecM(t *testing.T) {
exp := "-1+9-88+(88)"
exec(exp)
}
func TestExecN(t *testing.T) {
exp := "-1+9-88+(-88)*666-1"
exec(exp)
}
func TestExecO(t *testing.T) {
exp := "-(1)+(3)-(-3)*7-((-3))"
exec(exp)
}
func TestExecP(t *testing.T) {
exp := "-(-9+3)"
exec(exp)
}
func TestExecQ(t *testing.T) {
exp := "2e-3*2+2e2+1"
exec(exp)
}
func TestExecR(t *testing.T) {
exp := "3.8 - 56 / (1-1) - 4"
exec(exp)
}
func TestExecS(t *testing.T) {
exp := "noerr(3.8 - 56 / (1-1) - 4)"
exec(exp)
}
func TestFunCaller(t *testing.T) {
funs := []struct {
Name string
Argc int
Fun func(expr ...ExprAST) float64
Exp string
R float64
}{
//{
// "double",
// 1,
// func(expr ...engine.ExprAST) float64 {
// return engine.ExprASTResult(expr[0]) * 2
// },
// "double(6)",
// 12,
//},
{
"sum",
-1,
nil,
"sum(if(100+10,table.a,20))",
10,
},
{
"sum",
-1,
nil,
"sum(if(100<10,table.a,20))",
10,
},
{
"sum",
-1,
nil,
"sum(if(100<10,table.a,20+30))",
10,
},
{
"sum",
-1,
nil,
"sum(if(table.a<table.b,table.a,20+30))",
10,
},
{
"sum",
-1,
nil,
"sum(if(table.a<=table.b,table.a,20+30))",
10,
},
}
for _, f := range funs {
if f.Fun != nil {
_ = RegFunction(f.Name, f.Argc, f.Fun)
}
r, err := Parse(f.Exp)
if err != nil {
}
if r != 0 {
}
}
}
func TestFunCaller2(t *testing.T) {
funs := []struct {
Name string
Exp []string
}{
{
"sum",
[]string{"sum(table.a)"},
},
{
"sumif",
[]string{"sumif(table.month,10,table.count)"},
},
{
"if",
[]string{"if(table.month>10,table.count1,table.count2)"},
},
{
"and",
[]string{"and(table.year=2011,table.month=6)"},
},
{
"or",
[]string{"or(table.year=2011,table.year=2012)"},
},
{
"month",
[]string{"month(\"1991-1-1\")"},
},
{
"year",
[]string{"year(\"1991-1-1\")"},
},
{
"round",
[]string{
"round(1.56)",
"round(table.a)",
},
},
{
"rounddown",
[]string{
"rounddown(1.56)",
"rounddown(table.a)",
},
},
{
"roundup",
[]string{
"roundup(1.56)",
"roundup(table.a)",
},
},
{
"count",
[]string{
"count(1.56)",
"count(table.a)",
},
},
{
"&",
[]string{
"table.a&table.b",
},
},
}
for _, f := range funs {
for _, exp := range f.Exp {
r, err := Parse(exp)
if err != nil {
t.Error(err)
}
if r != 0 {
}
}
}
}
func Parse(s string) (r float64, err error) {
toks, err := ParseToken(s)
if err != nil {
return 0, err
}
ast := NewAST(toks, s)
if ast.Err != nil {
return 0, ast.Err
}
ar := ast.ParseExpression()
if ast.Err != nil {
return 0, ast.Err
}
defer func() {
if e := recover(); e != nil {
err = e.(error)
}
}()
buf := bytes.NewBuffer(nil)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.Encode(ar)
// if ar != nil {
// fmt.Printf("ExprAST: %+v\n", ar)
// }
fmt.Println(buf.String())
return 0, err
}
// call engine
func exec(exp string) {
// input text -> []token
toks, err := ParseToken(exp)
if err != nil {
fmt.Println("ERROR: " + err.Error())
return
}
// []token -> AST Tree
ast := NewAST(toks, exp)
if ast.Err != nil {
fmt.Println("ERROR: " + ast.Err.Error())
return
}
// AST builder
ar := ast.ParseExpression()
if ast.Err != nil {
fmt.Println("ERROR: " + ast.Err.Error())
return
}
fmt.Printf("ExprAST: %+v\n", ar)
// catch runtime errors
defer func() {
if e := recover(); e != nil {
fmt.Println("ERROR: ", e)
}
}()
// AST traversal -> result
r := ExprASTResult(ar)
fmt.Println("progressing ...\t", r)
fmt.Printf("%s = %v\n", exp, r)
}
... ...
package astexpr
import (
"errors"
"fmt"
"math"
"math/big"
"strconv"
"strings"
)
// Top level function
// Analytical expression and execution
// err is not nil if an error occurs (including arithmetic runtime errors)
func ParseAndExec(s string) (r float64, err error) {
toks, err := ParseToken(s)
if err != nil {
return 0, err
}
ast := NewAST(toks, s)
if ast.Err != nil {
return 0, ast.Err
}
ar := ast.ParseExpression()
if ast.Err != nil {
return 0, ast.Err
}
defer func() {
if e := recover(); e != nil {
err = e.(error)
}
}()
return ExprASTResult(ar), err
}
func ErrPos(s string, pos int) string {
r := strings.Repeat("-", len(s)) + "\n"
s += "\n"
for i := 0; i < pos; i++ {
s += " "
}
s += "^\n"
return r + s + r
}
// the integer power of a number
func Pow(x float64, n float64) float64 {
return math.Pow(x, n)
}
func expr2Radian(expr ExprAST) float64 {
r := ExprASTResult(expr)
if TrigonometricMode == AngleMode {
r = r / 180 * math.Pi
}
return r
}
// Float64ToStr float64 -> string
func Float64ToStr(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
// RegFunction is Top level function
// register a new function to use in expressions
// name: be register function name. the same function name only needs to be registered once.
// argc: this is a number of parameter signatures. should be -1, 0, or a positive integer
//
// -1 variable-length argument; >=0 fixed numbers argument
//
// fun: function handler
func RegFunction(name string, argc int, fun func(...ExprAST) float64) error {
if len(name) == 0 {
return errors.New("RegFunction name is not empty")
}
if argc < -1 {
return errors.New("RegFunction argc should be -1, 0, or a positive integer")
}
if _, ok := defFunc[name]; ok {
return errors.New("RegFunction name is already exist")
}
defFunc[name] = defS{argc, fun}
return nil
}
// ExprASTResult is a Top level function
// AST traversal
// if an arithmetic runtime error occurs, a panic exception is thrown
func ExprASTResult(expr ExprAST) float64 {
var l, r float64
switch expr.(type) {
case BinaryExprAST:
ast := expr.(BinaryExprAST)
l = ExprASTResult(ast.Lhs)
r = ExprASTResult(ast.Rhs)
switch ast.Op {
case "+":
lh, _ := new(big.Float).SetString(Float64ToStr(l))
rh, _ := new(big.Float).SetString(Float64ToStr(r))
f, _ := new(big.Float).Add(lh, rh).Float64()
return f
case "-":
lh, _ := new(big.Float).SetString(Float64ToStr(l))
rh, _ := new(big.Float).SetString(Float64ToStr(r))
f, _ := new(big.Float).Sub(lh, rh).Float64()
return f
case "*":
f, _ := new(big.Float).Mul(new(big.Float).SetFloat64(l), new(big.Float).SetFloat64(r)).Float64()
return f
case "/":
if r == 0 {
panic(errors.New(
fmt.Sprintf("violation of arithmetic specification: a division by zero in ExprASTResult: [%g/%g]",
l,
r)))
}
f, _ := new(big.Float).Quo(new(big.Float).SetFloat64(l), new(big.Float).SetFloat64(r)).Float64()
return f
case "%":
if r == 0 {
panic(errors.New(
fmt.Sprintf("violation of arithmetic specification: a division by zero in ExprASTResult: [%g%%%g]",
l,
r)))
}
return float64(int(l) % int(r))
case "^":
return Pow(l, r)
default:
}
case NumberExprAST:
return expr.(NumberExprAST).Val
case FunCallerExprAST:
f := expr.(FunCallerExprAST)
def := defFunc[f.Name]
return def.fun(f.Args...)
}
return 0.0
}
... ...
package astexpr
import (
"math/rand"
"testing"
"time"
)
func TestParseAndExecSimple(t *testing.T) {
type U struct {
Expr string
R float64
}
exprs := []U{
{"1", 1},
{"--1", 1},
{"1+2", 3},
{"-1+2", 1},
{"-(1+2)", -3},
{"-(1+2)*5", -15},
{"-(1+2)*5/3", -5},
{"1+(-(1+2)*5/3)", -4},
{"3^4", 81},
{"3^4.5", 140.29611541307906},
{"3.5^4.5", 280.7412308013823},
{"8%2", 0},
{"8%3", 2},
{"8%3.5", 2},
{"1e2", 100},
{"1e+2", 100},
{"1e-2", 0.01},
{"1e-2+1e2", 100.01},
{"1e-2+1e2*6/3", 200.01},
{"(1e-2+1e2)*6/3", 200.02},
{"(88*8)+(1+1+1+1)+(6/1.5)-(99%9*(2^4))", 712},
{"1/3*3", 1},
{"123_456_789", 123456789},
{"123_456_789___", 123456789},
{"pi", 3.141592653589793},
{"abs(1)", 1},
{"abs(-1)", 1},
{"ceil(90.2)", 91},
{"ceil(90.8)", 91},
{"ceil(90.0)", 90},
{"floor(90.2)", 90},
{"floor(90.8)", 90},
{"floor(90.0)", 90},
{"round(90.0)", 90},
{"round(90.4)", 90},
{"round(90.5)", 91},
{"round(90.9)", 91},
{"sqrt(4)", 2},
{"cbrt(27)", 3},
{"sqrt(4) + cbrt(27)", 5},
{"sqrt(2^2) + cbrt(3^3)", 5},
{"127^2+5/2-sqrt(2^2) + cbrt(3^3)", 16132.5},
{"max(2)", 2},
{"max(abs(1)+10)", 11},
{"max(abs(1)+10)*2-1", 21},
{"max(2,3.5)", 3.5},
{"max(2^3,3+abs(-1)*6)", 9},
{"max(2^3,3+abs(-1)*6, 20)", 20},
{"max(2^3,3+abs(-1)*6,ceil(9.4))", 10},
{"max(1,2,3,4,5,6,10,7,4,5,6,9.8)", 10},
{"min(3.5)", 3.5},
{"min(ceil(1.2))", 2},
{"min(2,3.5)", 2},
{"min(2^3,3+abs(-1)*6)", 8},
{"min(2^3,3+abs(-1)*6,1^10)", 1},
{"min(99.1,0.2,3,4,5,6,10,7,4,5,6,9.8)", 0.2},
{"max(2^3,3^2)", 9},
{"min(2^3,3^2)", 8},
{"noerr(1/0)", 0},
{"noerr(1/(1-1))", 0},
{"0.1+0.2", 0.3},
{"0.3-0.1", 0.2},
{"10^-1", 0.1},
{"10^-2", 0.01},
{"10^-1*100", 10},
{"10%0", 0},
}
for _, e := range exprs {
r, _ := ParseAndExec(e.Expr)
if r != e.R {
t.Error(e, " ParseAndExec:", r)
}
}
}
func TestParseAndExecTrigonometric(t *testing.T) {
type U struct {
Expr string
RadianMode float64
AngleMode float64
}
exprs := []U{
{"sin(pi/2)", 1, 0.027412133592044294},
{"csc(pi/2)", 1, 36.48019577324057},
{"cos(0)", 1, 1},
{"sec(0)", 1, 1},
{"tan(pi/4)", 1, 0.013708642534394057},
{"cot(pi/4)", 1, 72.94668290394674},
{"sin(90)", 0.893996663600558, 1},
{"csc(90)", 1.1185724071637082, 1},
{"cos(0)", 1, 1},
{"sec(0)", 1, 1},
{"tan(45)", 1.6197751905438615, 1},
{"cot(45)", 0.6173696237835551, 1},
}
for _, e := range exprs {
TrigonometricMode = RadianMode
r, _ := ParseAndExec(e.Expr)
if r != e.RadianMode {
t.Error(e, " ParseAndExec RadianMode:", r)
}
TrigonometricMode = AngleMode
r, _ = ParseAndExec(e.Expr)
if r != e.AngleMode {
t.Error(e, " ParseAndExec AngleMode:", r)
}
}
}
func TestRegFunction(t *testing.T) {
funs := []struct {
Name string
Argc int
Fun func(expr ...ExprAST) float64
Exp string
R float64
}{
{
"double",
1,
func(expr ...ExprAST) float64 {
return ExprASTResult(expr[0]) * 2
},
"double(6)",
12,
},
{
"percentage50",
1,
func(expr ...ExprAST) float64 {
return ExprASTResult(expr[0]) / 2
},
"percentage50(6)",
3,
},
{
"range",
0,
func(expr ...ExprAST) float64 {
return 10.0
},
"range()",
10,
},
{
"choice",
-1,
func(expr ...ExprAST) float64 {
rand.Seed(time.Now().UnixNano())
return ExprASTResult(expr[rand.Intn(len(expr))])
},
"choice(1.1, 9.8, 2.5, 100)",
10,
},
}
for _, f := range funs {
_ = RegFunction(f.Name, f.Argc, f.Fun)
r, err := ParseAndExec(f.Exp)
if f.Name == "choice" {
if !inSlices(r, []float64{1.1, 9.8, 2.5, 100}) {
t.Error(err, "RegFunction errors when register new function: ", f.Name)
}
continue
} else if r != f.R {
t.Error(err, "RegFunction errors when register new function: ", f.Name)
}
}
}
func TestParseAndExecError(t *testing.T) {
exprs := []string{
"(",
"((((((",
"((xscdfddff",
"(1",
"(1+",
"1+",
"1*",
"+2344",
"3+(",
"4+(90-",
"3-(4*7-2)+",
"3-(4*7-2)+98*",
"1#1",
"_123_456_789___",
"1ee3+3",
"sin()",
"sin",
"pi(",
"sin(1, 50)",
"max",
"max()",
"max(1,)",
"max(1,4,6,7,5,)",
"min",
"min(,)",
"min()",
"min(1,)",
"min(1,998,4,23,234,2,)",
"min(1,998,4,23,234,2,,,)",
"1/0",
"99.9 / (2-1-1)",
"(1+2)3",
"1+1 111",
"1+1 111+2",
"1 3",
"1 3-",
}
for _, e := range exprs {
_, err := ParseAndExec(e)
if err == nil {
t.Error(e, " this is error expr!")
}
}
}
func inSlices(target float64, s []float64) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
... ...
... ... @@ -160,7 +160,7 @@ func (t TableType) ToString() string {
}
func (t TableType) TableStatusEditable() bool {
return t == SchemaTable || t == CalculateItem || t == CalculateTable || t == CalculateSet
return t == SchemaTable || t == CalculateItem || t == CalculateSet
}
func (t TableType) TableHasGroup() bool {
... ...
... ... @@ -28,6 +28,16 @@ type Field struct {
Order string `json:"order,omitempty"`
}
func (f *Field) SqlTypeEqual(compare *Field) bool {
if f.SQLType == compare.SQLType {
return true
}
if SQLType(f.SQLType).IsString() == SQLType(compare.SQLType).IsString() {
return true
}
return false
}
func (f *Field) Valid() error {
if _, ok := SQLTypeMap[strings.ToUpper(f.SQLType)]; !ok {
return fmt.Errorf("unknown sql type:%v", f.SQLType)
... ...
... ... @@ -133,6 +133,25 @@ func (querySet *QuerySet) GetDependencyTables(queryComponents []*QueryComponent)
return res
}
func (querySet *QuerySet) Valid(queryComponents []*QueryComponent) error {
switch querySet.Type {
case CalculateTable.ToString():
if len(queryComponents) == 0 {
return fmt.Errorf("行、值不能同时为空")
}
qc := queryComponents[0]
set := collection.NewSet()
for _, f := range qc.Aggregation.AggregationFields() {
if !set.Contains(f.DisplayName) {
set.AddStr(f.DisplayName)
} else {
return fmt.Errorf("字段'%s'存在重名,请进行重命名", f.DisplayName)
}
}
}
return nil
}
type QuerySets []*QuerySet
func (querySets QuerySets) ToMap() map[int]*QuerySet {
... ...
... ... @@ -67,7 +67,7 @@ func (table *Table) TableIdString() string {
func (table *Table) WithContext(ctx *Context) *Table {
rand.Seed(time.Now().Unix())
table.SQLName = fmt.Sprintf("%v_t%v_c%v", limitStringLen(table.SQLName, 40), rand.Intn(1000000), limitStringLen(fmt.Sprintf("%d", ctx.CompanyId), 8))
table.SQLName = fmt.Sprintf("%v_t%v_c%v", limitStringLen(table.SQLName, 30), rand.Intn(1000000), limitStringLen(fmt.Sprintf("%d", ctx.CompanyId), 8))
table.Context = ctx
return table
}
... ...
... ... @@ -188,7 +188,7 @@ func NewTableAppendRequest(param domain.ReqAppendData) TableAppendRequest {
OriginalTableId: intToString(param.FileId),
CheckoutTableFileUrl: param.FileUrl,
DatabaseTableName: param.Table.SQLName,
ColumnSchemas: DomainFieldsToColumnSchemas(param.ExcelTable.DataFields),
ColumnSchemas: DomainFieldsToColumnSchemas(param.From), //param.ExcelTable.DataFields
FieldSchemas: ToFieldSchemas(param.Table.DataFields),
SchemaMap: make(map[string]domain.ColumnSchema),
}
... ...
... ... @@ -118,6 +118,9 @@ func (d *FilePreviewDto) Load(fileId int, m *domain.DataLoadDataTable, file *red
if s == "<NA>" {
return ""
}
if s == "nan" {
return ""
}
return s
}), true)
d.Data = domain.GripData(mapData, int64(m.Total))
... ...
... ... @@ -8,6 +8,7 @@ import (
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/infrastructure/redis"
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/infrastructure/repository"
"gitlab.fjmaimaimai.com/allied-creation/character-library-metadata-bastion/pkg/infrastructure/starrocks"
"reflect"
"strings"
"time"
)
... ... @@ -273,6 +274,9 @@ func (ptr *QuerySetService) PreviewPrepare(ctx *domain.Context, querySetId int,
if err != nil {
return nil, err
}
if err = querySet.Valid(queryComponents); err != nil {
return nil, err
}
if !queryComponentsHasEdit(ctx, querySet, queryComponents) && querySet.QuerySetInfo.BindTableId > 0 {
if t, _ := tableRepository.FindOne(map[string]interface{}{"context": ctx, "tableId": querySet.QuerySetInfo.BindTableId}); t != nil {
return t, nil
... ... @@ -655,6 +659,29 @@ func aggregationEditLog(ctx *domain.Context, querySet *domain.QuerySet, queryCom
return res
}
func aggregationHasEdit(ctx *domain.Context, querySet *domain.QuerySet, queryComponents []*domain.QueryComponent) bool {
if len(queryComponents) > 0 && len(querySet.QueryComponents) > 0 {
oldQC := querySet.QueryComponents[0]
newQC := queryComponents[0]
if oldQC.Aggregation == nil || newQC.Aggregation == nil {
return false
}
c1 := make([]string, 0)
for _, f := range oldQC.Aggregation.AggregationFields() {
c1 = append(c1, f.Expr.ExprSql)
}
c2 := make([]string, 0)
for _, f := range newQC.Aggregation.AggregationFields() {
c2 = append(c2, f.Expr.ExprSql)
}
if !reflect.DeepEqual(c1, c2) {
return true
}
}
return false
}
func queryComponentsHasEdit(ctx *domain.Context, querySet *domain.QuerySet, queryComponents []*domain.QueryComponent) bool {
logs := selectsEditLog(ctx, querySet, queryComponents)
if len(logs) > 0 {
... ... @@ -668,6 +695,9 @@ func queryComponentsHasEdit(ctx *domain.Context, querySet *domain.QuerySet, quer
if len(logs) > 0 {
return true
}
if aggregationHasEdit(ctx, querySet, queryComponents) {
return true
}
for _, item := range queryComponents {
if len(item.Id) == 0 {
return true
... ... @@ -757,6 +787,9 @@ func (ptr *QuerySetService) CreateOrUpdateCalculateTable(ctx *domain.Context, qu
err error
foundMasterTable *domain.Table
)
if err = querySet.Valid(queryComponents); err != nil {
return nil, err
}
dependencyTables := querySet.GetDependencyTables(queryComponents)
tableRepository, _ := repository.NewTableRepository(ptr.transactionContext)
foundMasterTable, err = tableRepository.FindOne(map[string]interface{}{"context": ctx, "tableId": masterTable.TableId})
... ...
... ... @@ -79,6 +79,13 @@ func (ptr *AppendDataToTableService) AppendData(ctx *domain.Context, fileId int,
continue
}
}
if fromField.SQLType != "" && !toField.SqlTypeEqual(fromField) {
//return nil, fmt.Errorf("字段【%s】的类型与导入数据表的类型不匹配", toField.Name)
return map[string]interface{}{
"result": fmt.Sprintf("字段【%s】的类型与导入数据表的类型不匹配", toField.Name),
}, nil
}
fromField.SQLType = toField.SQLType // 兼容 INT BIGINT
requestData.To = append(requestData.To, toField)
requestData.From = append(requestData.From, fromField)
}
... ...