| package govaluate |
| |
| import ( |
| "errors" |
| "fmt" |
| "sync" |
| ) |
| |
| const isoDateFormat string = "2006-01-02T15:04:05.999999999Z0700" |
| const shortCircuitHolder int = -1 |
| |
| var DUMMY_PARAMETERS = MapParameters(map[string]interface{}{}) |
| |
| /* |
| EvaluableExpression represents a set of ExpressionTokens which, taken together, |
| are an expression that can be evaluated down into a single value. |
| */ |
| type EvaluableExpression struct { |
| |
| /* |
| Represents the query format used to output dates. Typically only used when creating SQL or Mongo queries from an expression. |
| Defaults to the complete ISO8601 format, including nanoseconds. |
| */ |
| QueryDateFormat string |
| |
| /* |
| Whether or not to safely check types when evaluating. |
| If true, this library will return error messages when invalid types are used. |
| If false, the library will panic when operators encounter types they can't use. |
| |
| This is exclusively for users who need to squeeze every ounce of speed out of the library as they can, |
| and you should only set this to false if you know exactly what you're doing. |
| */ |
| ChecksTypes bool |
| |
| tokens []ExpressionToken |
| evaluationStages *evaluationStage |
| inputExpression string |
| } |
| |
| /* |
| Parses a new EvaluableExpression from the given [expression] string. |
| Returns an error if the given expression has invalid syntax. |
| */ |
| func NewEvaluableExpression(expression string) (*EvaluableExpression, error) { |
| |
| functions := make(map[string]ExpressionFunction) |
| return NewEvaluableExpressionWithFunctions(expression, functions) |
| } |
| |
| /* |
| Similar to [NewEvaluableExpression], except that instead of a string, an already-tokenized expression is given. |
| This is useful in cases where you may be generating an expression automatically, or using some other parser (e.g., to parse from a query language) |
| */ |
| func NewEvaluableExpressionFromTokens(tokens []ExpressionToken) (*EvaluableExpression, error) { |
| |
| var ret *EvaluableExpression |
| var err error |
| |
| ret = new(EvaluableExpression) |
| ret.QueryDateFormat = isoDateFormat |
| |
| err = checkBalance(tokens) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = checkExpressionSyntax(tokens) |
| if err != nil { |
| return nil, err |
| } |
| |
| ret.tokens, err = optimizeTokens(tokens) |
| if err != nil { |
| return nil, err |
| } |
| |
| ret.evaluationStages, err = planStages(ret.tokens) |
| if err != nil { |
| return nil, err |
| } |
| |
| ret.ChecksTypes = true |
| return ret, nil |
| } |
| |
| /* |
| Similar to [NewEvaluableExpression], except enables the use of user-defined functions. |
| Functions passed into this will be available to the expression. |
| */ |
| func NewEvaluableExpressionWithFunctions(expression string, functions map[string]ExpressionFunction) (*EvaluableExpression, error) { |
| |
| var ret *EvaluableExpression |
| var err error |
| |
| ret = new(EvaluableExpression) |
| ret.QueryDateFormat = isoDateFormat |
| ret.inputExpression = expression |
| |
| ret.tokens, err = parseTokens(expression, functions) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = checkBalance(ret.tokens) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = checkExpressionSyntax(ret.tokens) |
| if err != nil { |
| return nil, err |
| } |
| |
| ret.tokens, err = optimizeTokens(ret.tokens) |
| if err != nil { |
| return nil, err |
| } |
| |
| ret.evaluationStages, err = planStages(ret.tokens) |
| if err != nil { |
| return nil, err |
| } |
| |
| ret.ChecksTypes = true |
| return ret, nil |
| } |
| |
| /* |
| Same as `Eval`, but automatically wraps a map of parameters into a `govalute.Parameters` structure. |
| */ |
| func (e EvaluableExpression) Evaluate(parameters map[string]interface{}) (interface{}, error) { |
| |
| if parameters == nil { |
| return e.Eval(nil) |
| } |
| |
| return e.Eval(MapParameters(parameters)) |
| } |
| |
| var sanitizedParamsPool = sync.Pool{ |
| New: func() interface{} { |
| return &sanitizedParameters{} |
| }, |
| } |
| |
| /* |
| Runs the entire expression using the given [parameters]. |
| e.g., If the expression contains a reference to the variable "foo", it will be taken from `parameters.Get("foo")`. |
| |
| This function returns errors if the combination of expression and parameters cannot be run, |
| such as if a variable in the expression is not present in [parameters]. |
| |
| In all non-error circumstances, this returns the single value result of the expression and parameters given. |
| e.g., if the expression is "1 + 1", this will return 2.0. |
| e.g., if the expression is "foo + 1" and parameters contains "foo" = 2, this will return 3.0 |
| */ |
| func (e EvaluableExpression) Eval(parameters Parameters) (interface{}, error) { |
| |
| if e.evaluationStages == nil { |
| return nil, nil |
| } |
| |
| free := false |
| if parameters != nil { |
| free = true |
| tmp := sanitizedParamsPool.Get().(*sanitizedParameters) |
| tmp.orig = parameters |
| parameters = tmp |
| } else { |
| parameters = DUMMY_PARAMETERS |
| } |
| |
| ret, err := e.evaluateStage(e.evaluationStages, parameters) |
| if free { |
| sanitizedParamsPool.Put(parameters) |
| } |
| return ret, err |
| } |
| |
| func (e EvaluableExpression) evaluateStage(stage *evaluationStage, parameters Parameters) (interface{}, error) { |
| |
| var left, right interface{} |
| var err error |
| |
| if stage.leftStage != nil { |
| left, err = e.evaluateStage(stage.leftStage, parameters) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| if stage.isShortCircuitable() { |
| switch stage.symbol { |
| case AND: |
| if left == false { |
| return false, nil |
| } |
| case OR: |
| if left == true { |
| return true, nil |
| } |
| case COALESCE: |
| if left != nil { |
| return left, nil |
| } |
| |
| case TERNARY_TRUE: |
| if left == false { |
| right = shortCircuitHolder |
| } |
| case TERNARY_FALSE: |
| if left != nil { |
| right = shortCircuitHolder |
| } |
| } |
| } |
| |
| if right != shortCircuitHolder && stage.rightStage != nil { |
| right, err = e.evaluateStage(stage.rightStage, parameters) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| if e.ChecksTypes { |
| if stage.typeCheck == nil { |
| |
| err = typeCheck(stage.leftTypeCheck, left, stage.symbol, stage.typeErrorFormat) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = typeCheck(stage.rightTypeCheck, right, stage.symbol, stage.typeErrorFormat) |
| if err != nil { |
| return nil, err |
| } |
| } else { |
| // special case where the type check needs to know both sides to determine if the operator can handle it |
| if !stage.typeCheck(left, right) { |
| errorMsg := fmt.Sprintf(stage.typeErrorFormat, left, stage.symbol.String()) |
| return nil, errors.New(errorMsg) |
| } |
| } |
| } |
| |
| return stage.operator(left, right, parameters) |
| } |
| |
| func typeCheck(check stageTypeCheck, value interface{}, symbol OperatorSymbol, format string) error { |
| |
| if check == nil { |
| return nil |
| } |
| |
| if check(value) { |
| return nil |
| } |
| |
| errorMsg := fmt.Sprintf(format, value, symbol.String()) |
| return errors.New(errorMsg) |
| } |
| |
| /* |
| Returns an array representing the ExpressionTokens that make up this expression. |
| */ |
| func (e EvaluableExpression) Tokens() []ExpressionToken { |
| |
| return e.tokens |
| } |
| |
| /* |
| Returns the original expression used to create this EvaluableExpression. |
| */ |
| func (e EvaluableExpression) String() string { |
| |
| return e.inputExpression |
| } |
| |
| /* |
| Returns an array representing the variables contained in this EvaluableExpression. |
| */ |
| func (e EvaluableExpression) Vars() []string { |
| var varlist []string |
| for _, val := range e.Tokens() { |
| if val.Kind == VARIABLE { |
| varlist = append(varlist, val.Value.(string)) |
| } |
| } |
| return varlist |
| } |
| |
| /* |
| Removes the tokens from the EvaluableExpression. This will cause the Tokens() and Vars() functions to no longer operate, but will save memory. |
| */ |
| func (e *EvaluableExpression) CleanupTokens() { |
| e.tokens = e.tokens[:0] |
| } |
| |
| /* |
| EvaluationResult represents the result of evaluating an expression with a set of parameters. |
| */ |
| type EvaluationResult struct { |
| // Result is the evaluation result, or nil if there was an error |
| Result interface{} |
| // Error is the error that occurred during evaluation, or nil if successful |
| Error error |
| } |
| |
| /* |
| EvaluateBatch evaluates the expression against multiple parameter sets sequentially. |
| This is useful when you need to evaluate the same expression against many different parameter sets. |
| |
| Returns a slice of EvaluationResult in the same order as the input parameterSets. |
| */ |
| func (e EvaluableExpression) EvaluateBatch(parameterSets []map[string]interface{}) []EvaluationResult { |
| results := make([]EvaluationResult, len(parameterSets)) |
| |
| for i, params := range parameterSets { |
| result, err := e.Evaluate(params) |
| results[i] = EvaluationResult{ |
| Result: result, |
| Error: err, |
| } |
| } |
| |
| return results |
| } |
| |
| /* |
| EvaluateBatchParallel evaluates the expression against multiple parameter sets in parallel. |
| This method is thread-safe and can provide significant performance improvements when evaluating |
| the same expression against many different parameter sets. |
| |
| The maxWorkers parameter controls the maximum number of concurrent goroutines used for evaluation. |
| If maxWorkers is <= 0, it defaults to the number of parameter sets (fully parallel). |
| |
| Returns a slice of EvaluationResult in the same order as the input parameterSets. |
| |
| Example: |
| |
| expression, _ := govaluate.NewEvaluableExpression("foo > threshold") |
| paramSets := []map[string]interface{}{ |
| {"foo": 10, "threshold": 5}, |
| {"foo": 3, "threshold": 5}, |
| {"foo": 7, "threshold": 5}, |
| } |
| results := expression.EvaluateBatchParallel(paramSets, 0) |
| for i, result := range results { |
| if result.Error != nil { |
| fmt.Printf("Error evaluating set %d: %v\n", i, result.Error) |
| } else { |
| fmt.Printf("Result %d: %v\n", i, result.Result) |
| } |
| } |
| */ |
| func (e EvaluableExpression) EvaluateBatchParallel(parameterSets []map[string]interface{}, maxWorkers int) []EvaluationResult { |
| numSets := len(parameterSets) |
| if numSets == 0 { |
| return []EvaluationResult{} |
| } |
| |
| // Determine the number of workers |
| workers := maxWorkers |
| if workers <= 0 { |
| workers = numSets |
| } |
| |
| results := make([]EvaluationResult, numSets) |
| |
| // Create a work queue |
| type work struct { |
| index int |
| params map[string]interface{} |
| } |
| |
| workChan := make(chan work, numSets) |
| |
| // Start workers |
| var wg sync.WaitGroup |
| wg.Add(workers) |
| |
| for w := 0; w < workers; w++ { |
| go func() { |
| defer wg.Done() |
| for job := range workChan { |
| result, err := e.Evaluate(job.params) |
| results[job.index] = EvaluationResult{ |
| Result: result, |
| Error: err, |
| } |
| } |
| }() |
| } |
| |
| // Queue all work |
| for i, params := range parameterSets { |
| workChan <- work{index: i, params: params} |
| } |
| close(workChan) |
| |
| // Wait for all workers to complete |
| wg.Wait() |
| |
| return results |
| } |