govaluate

package module
v3.0.0+incompatible Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 1, 2017 License: MIT Imports: 9 Imported by: 973

README

govaluate

Build Status Godoc

Provides support for evaluating arbitrary C-like artithmetic/string expressions.

Why can't you just write these expressions in code?

Sometimes, you can't know ahead-of-time what an expression will look like, or you want those expressions to be configurable. Perhaps you've got a set of data running through your application, and you want to allow your users to specify some validations to run on it before committing it to a database. Or maybe you've written a monitoring framework which is capable of gathering a bunch of metrics, then evaluating a few expressions to see if any metrics should be alerted upon, but the conditions for alerting are different for each monitor.

A lot of people wind up writing their own half-baked style of evaluation language that fits their needs, but isn't complete. Or they wind up baking the expression into the actual executable, even if they know it's subject to change. These strategies may work, but they take time to implement, time for users to learn, and induce technical debt as requirements change. This library is meant to cover all the normal C-like expressions, so that you don't have to reinvent one of the oldest wheels on a computer.

How do I use it?

You create a new EvaluableExpression, then call "Evaluate" on it.

	expression, err := govaluate.NewEvaluableExpression("10 > 0");
	result, err := expression.Evaluate(nil);
	// result is now set to "true", the bool value.

Cool, but how about with parameters?

	expression, err := govaluate.NewEvaluableExpression("foo > 0");

	parameters := make(map[string]interface{}, 8)
	parameters["foo"] = -1;

	result, err := expression.Evaluate(parameters);
	// result is now set to "false", the bool value.

That's cool, but we can almost certainly have done all that in code. What about a complex use case that involves some math?

	expression, err := govaluate.NewEvaluableExpression("(requests_made * requests_succeeded / 100) >= 90");

	parameters := make(map[string]interface{}, 8)
	parameters["requests_made"] = 100;
	parameters["requests_succeeded"] = 80;

	result, err := expression.Evaluate(parameters);
	// result is now set to "false", the bool value.

Or maybe you want to check the status of an alive check ("smoketest") page, which will be a string?

	expression, err := govaluate.NewEvaluableExpression("http_response_body == 'service is ok'");

	parameters := make(map[string]interface{}, 8)
	parameters["http_response_body"] = "service is ok";

	result, err := expression.Evaluate(parameters);
	// result is now set to "true", the bool value.

These examples have all returned boolean values, but it's equally possible to return numeric ones.

	expression, err := govaluate.NewEvaluableExpression("(mem_used / total_mem) * 100");

	parameters := make(map[string]interface{}, 8)
	parameters["total_mem"] = 1024;
	parameters["mem_used"] = 512;

	result, err := expression.Evaluate(parameters);
	// result is now set to "50.0", the float64 value.

You can also do date parsing, though the formats are somewhat limited. Stick to RF3339, ISO8061, unix date, or ruby date formats. If you're having trouble getting a date string to parse, check the list of formats actually used: parsing.go:248.

	expression, err := govaluate.NewEvaluableExpression("'2014-01-02' > '2014-01-01 23:59:59'");
	result, err := expression.Evaluate(nil);

	// result is now set to true

Expressions are parsed once, and can be re-used multiple times. Parsing is the compute-intensive phase of the process, so if you intend to use the same expression with different parameters, just parse it once. Like so;

	expression, err := govaluate.NewEvaluableExpression("response_time <= 100");
	parameters := make(map[string]interface{}, 8)

	for {
		parameters["response_time"] = pingSomething();
		result, err := expression.Evaluate(parameters)
	}

The normal C-standard order of operators is respected. When writing an expression, be sure that you either order the operators correctly, or use parenthesis to clarify which portions of an expression should be run first.

Escaping characters

Sometimes you'll have parameters that have spaces, slashes, pluses, ampersands or some other character that this library interprets as something special. For example, the following expression will not act as one might expect:

"response-time < 100"

As written, the library will parse it as "[response] minus [time] is less than 100". In reality, "response-time" is meant to be one variable that just happens to have a dash in it.

There are two ways to work around this. First, you can escape the entire parameter name:

"[response-time] < 100"

Or you can use backslashes to escape only the minus sign.

"response\\-time < 100"

Backslashes can be used anywhere in an expression to escape the very next character. Square bracketed parameter names can be used instead of plain parameter names at any time.

Functions

You may have cases where you want to call a function on a parameter during execution of the expression. Perhaps you want to aggregate some set of data, but don't know the exact aggregation you want to use until you're writing the expression itself. Or maybe you have a mathematical operation you want to perform, for which there is no operator; like log or tan or sqrt. For cases like this, you can provide a map of functions to NewEvaluableExpressionWithFunctions, which will then be able to use them during execution. For instance;

	functions := map[string]govaluate.ExpressionFunction {
		"strlen": func(args ...interface{}) (interface{}, error) {
			length := len(args[0].(string))
			return (float64)(length), nil
		},
	}

	expString := "strlen('someReallyLongInputString') <= 16"
	expression, _ := govaluate.NewEvaluableExpressionWithFunctions(expString, functions)

	result, _ := expression.Evaluate(nil)
	// result is now "false", the boolean value

Functions can accept any number of arguments, correctly handles nested functions, and arguments can be of any type (even if none of this library's operators support evaluation of that type). For instance, each of these usages of functions in an expression are valid (assuming that the appropriate functions and parameters are given):

"sqrt(x1 ** y1, x2 ** y2)"
"max(someValue, abs(anotherValue), 10 * lastValue)"

Functions cannot be passed as parameters, they must be known at the time when the expression is parsed, and are unchangeable after parsing.

What operators and types does this support?

  • Modifiers: + - / * & | ^ ** % >> <<
  • Comparators: > >= < <= == != =~ !~
  • Logical ops: || &&
  • Numeric constants, as 64-bit floating point (12345.678)
  • String constants (single quotes: 'foobar')
  • Date constants (single quotes, using any permutation of RFC3339, ISO8601, ruby date, or unix date; date parsing is automatically tried with any string constant)
  • Boolean constants: true false
  • Parenthesis to control order of evaluation ( )
  • Arrays (anything separated by , within parenthesis: (1, 2, 'foo'))
  • Prefixes: ! - ~
  • Ternary conditional: ? :
  • Null coalescence: ??

See MANUAL.md for exacting details on what types each operator supports.

Types

Some operators don't make sense when used with some types. For instance, what does it mean to get the modulo of a string? What happens if you check to see if two numbers are logically AND'ed together?

Everyone has a different intuition about the answers to these questions. To prevent confusion, this library will refuse to operate upon types for which there is not an unambiguous meaning for the operation. See MANUAL.md for details about what operators are valid for which types.

Benchmarks

If you're concerned about the overhead of this library, a good range of benchmarks are built into this repo. You can run them with go test -bench=.. The library is built with an eye towards being quick, but has not been aggressively profiled and optimized. For most applications, though, it is completely fine.

For a very rough idea of performance, here are the results output from a benchmark run on a 3rd-gen Macbook Pro (Linux Mint 17.1).

BenchmarkSingleParse-12                          1000000              1382 ns/op
BenchmarkSimpleParse-12                           200000             10771 ns/op
BenchmarkFullParse-12                              30000             49383 ns/op
BenchmarkEvaluationSingle-12                    50000000                30.1 ns/op
BenchmarkEvaluationNumericLiteral-12            10000000               119 ns/op
BenchmarkEvaluationLiteralModifiers-12          10000000               236 ns/op
BenchmarkEvaluationParameters-12                 5000000               260 ns/op
BenchmarkEvaluationParametersModifiers-12        3000000               547 ns/op
BenchmarkComplexExpression-12                    2000000               963 ns/op
BenchmarkRegexExpression-12                       100000             20357 ns/op
BenchmarkConstantRegexExpression-12              1000000              1392 ns/op
ok

API Breaks

While this library has very few cases which will ever result in an API break, it can (and has) happened. If you are using this in production, vendor the commit you've tested against, or use gopkg.in to redirect your import (e.g., import "gopkg.in/Knetic/govaluate.v2"). Master branch (while infrequent) may at some point contain API breaking changes, and the author will have no way to communicate these to downstreams, other than creating a new major release.

Releases will explicitly state when an API break happens, and if they do not specify an API break it should be safe to upgrade.

License

This project is licensed under the MIT general use license. You're free to integrate, fork, and play with this code as you feel fit without consulting the author, as long as you provide proper credit to the author in your works.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DUMMY_PARAMETERS = MapParameters(map[string]interface{}{})

Functions

This section is empty.

Types

type EvaluableExpression

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
	// contains filtered or unexported fields
}

EvaluableExpression represents a set of ExpressionTokens which, taken together, are an expression that can be evaluated down into a single value.

func NewEvaluableExpression

func NewEvaluableExpression(expression string) (*EvaluableExpression, error)

Parses a new EvaluableExpression from the given [expression] string. Returns an error if the given expression has invalid syntax.

func NewEvaluableExpressionFromTokens

func NewEvaluableExpressionFromTokens(tokens []ExpressionToken) (*EvaluableExpression, error)

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 NewEvaluableExpressionWithFunctions

func NewEvaluableExpressionWithFunctions(expression string, functions map[string]ExpressionFunction) (*EvaluableExpression, error)

Similar to NewEvaluableExpression, except enables the use of user-defined functions. Functions passed into this will be available to the expression.

func (EvaluableExpression) Eval added in v1.5.0

func (this EvaluableExpression) Eval(parameters Parameters) (interface{}, error)

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 (EvaluableExpression) Evaluate

func (this EvaluableExpression) Evaluate(parameters map[string]interface{}) (interface{}, error)

Same as `Eval`, but automatically wraps a map of parameters into a `govalute.Parameters` structure.

func (EvaluableExpression) String

func (this EvaluableExpression) String() string

Returns the original expression used to create this EvaluableExpression.

func (EvaluableExpression) ToSQLQuery added in v1.2.0

func (this EvaluableExpression) ToSQLQuery() (string, error)

Returns a string representing this expression as if it were written in SQL. This function assumes that all parameters exist within the same table, and that the table essentially represents a serialized object of some sort (e.g., hibernate). If your data model is more normalized, you may need to consider iterating through each actual token given by `Tokens()` to create your query.

Boolean values are considered to be "1" for true, "0" for false.

Times are formatted according to this.QueryDateFormat.

func (EvaluableExpression) Tokens

func (this EvaluableExpression) Tokens() []ExpressionToken

Returns an array representing the ExpressionTokens that make up this expression.

func (EvaluableExpression) Vars

func (this EvaluableExpression) Vars() []string

Returns an array representing the variables contained in this EvaluableExpression.

type ExpressionFunction

type ExpressionFunction func(arguments ...interface{}) (interface{}, error)

Represents a function that can be called from within an expression. This method must return an error if, for any reason, it is unable to produce exactly one unambiguous result. An error returned will halt execution of the expression.

type ExpressionToken

type ExpressionToken struct {
	Kind  TokenKind
	Value interface{}
}

Represents a single parsed token.

type MapParameters added in v1.5.0

type MapParameters map[string]interface{}

func (MapParameters) Get added in v1.5.0

func (p MapParameters) Get(name string) (interface{}, error)

type OperatorSymbol

type OperatorSymbol int

Represents the valid symbols for operators.

const (
	VALUE OperatorSymbol = iota
	LITERAL
	NOOP
	EQ
	NEQ
	GT
	LT
	GTE
	LTE
	REQ
	NREQ
	IN

	AND
	OR

	PLUS
	MINUS
	BITWISE_AND
	BITWISE_OR
	BITWISE_XOR
	BITWISE_LSHIFT
	BITWISE_RSHIFT
	MULTIPLY
	DIVIDE
	MODULUS
	EXPONENT

	NEGATE
	INVERT
	BITWISE_NOT

	TERNARY_TRUE
	TERNARY_FALSE
	COALESCE

	FUNCTIONAL
	SEPARATE
)

func (OperatorSymbol) IsModifierType added in v1.4.0

func (this OperatorSymbol) IsModifierType(candidate []OperatorSymbol) bool

Returns true if this operator is contained by the given array of candidate symbols. False otherwise.

func (OperatorSymbol) String

func (this OperatorSymbol) String() string

Generally used when formatting type check errors. We could store the stringified symbol somewhere else and not require a duplicated codeblock to translate OperatorSymbol to string, but that would require more memory, and another field somewhere. Adding operators is rare enough that we just stringify it here instead.

type Parameters added in v1.5.0

type Parameters interface {

	/*
		Get gets the parameter of the given name, or an error if the parameter is unavailable.
		Failure to find the given parameter should be indicated by returning an error.
	*/
	Get(name string) (interface{}, error)
}

Parameters is a collection of named parameters that can be used by an EvaluableExpression to retrieve parameters when an expression tries to use them.

type TokenKind

type TokenKind int

Represents all valid types of tokens that a token can be.

const (
	UNKNOWN TokenKind = iota

	PREFIX
	NUMERIC
	BOOLEAN
	STRING
	PATTERN
	TIME
	VARIABLE
	FUNCTION
	SEPARATOR

	COMPARATOR
	LOGICALOP
	MODIFIER

	CLAUSE
	CLAUSE_CLOSE

	TERNARY
)

func (TokenKind) String

func (kind TokenKind) String() string

GetTokenKindString returns a string that describes the given TokenKind. e.g., when passed the NUMERIC TokenKind, this returns the string "NUMERIC".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL