blob: 749cd64920691f246e496603ed31d50d1114ebdb [file]
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
}