Sonic
sonic is a rapid evaluation engine for mathematical expressions. It can parse and execute strings containing mathematical expressions.
Install / Use
/learn @adletec/SonicREADME
<img alt="Our beautiful sonic logo" src="https://raw.githubusercontent.com/adletec/sonic/main/.resources/adletec_sonic_logo_64x64.png" width="24"/> sonic | rapid expression evaluation for .NET
sonic is a rapid evaluation engine for mathematical expressions. It can parse and evaluate strings containing mathematical expressions.
sonic is also the expression evaluator we use in our commercial products. It is a core component of our real-time simulation tools for virtual vehicle and ADAS prototyping and is continuously stress tested in a demanding environment. Its development and maintenance is funded by our product sales.
Guiding Principles
The guiding principles for sonic are (in that order):
- Performance: sonic is aiming to be the fastest expression evaluator for .NET. It is optimized for both, multi pass evaluation of the same expression and single pass evaluation of many different expressions.
- Usability: sonic is designed to be easy to use. It comes with a sane default configuration, an understandable documentation and a simple API. The most common use-cases should be fast out-of-the-box.
- Maintainability: sonic is designed to be easy to maintain. It is written in a clean and readable code style and comes with a comprehensive test and benchmarking suite. The NuGet package introduces no transient dependencies.
Quick Start
sonic can parse and evaluate strings containing mathematical expressions. These expressions may rely on variables, which can be defined at runtime.
Consider this example:
var expression = "var1*var2";
var variables = new Dictionary<string, double>();
variables.Add("var1", 2.5);
variables.Add("var2", 3.4);
var engine = Evaluator.CreateWithDefaults();
double result = engine.Evaluate(expression, variables); // 8.5
The Evaluator comes with out-of-the-box support for many arithmetic (+, -, *, /, ...),
trigonometric (sin, cos, atan, ...) statistic (avg, max, min, median, ...), and simple boolean
logic (if, ifless, ifequal, ...) functions.
You can add your own domain-specific functions. This example adds a conversion function from length in feet (ft) to
meter (m):
var engine = Evaluator.Create()
.AddFunction("ft2m", (Func<double, double>)((a) => a * 0.3048))
.Build();
double result = engine.Evaluate("ft2m(30)"); // 9.144
You can find more examples below.
sonic can execute formulas in two modes: dynamic compilation mode and interpreted mode. If dynamic compilation mode is used, sonic will create a dynamic method at runtime and will generate the MSIL opcodes necessary for the native execution of the evaluation. If a formula is re-evaluated with other variables, sonic will take the dynamically generated method from its cache (if enabled, which it is by default). Dynamic compilation mode is a lot faster when evaluating an expression, but has a higher overhead when building the evaluator.
As a rule of thumb, you should use dynamic compilation mode if you are evaluating the same expressions multiple times with different variables, and interpreted mode if you are evaluating many different expressions only once.
Additionally, for specific use-cases (e.g. Unity with IL2CPP) dynamic code generation can be limited. In those cases, you can use the interpreted mode as a fallback.
Migration from Jace.NET
sonic originally started as a fork of Jace.NET by Pieter De Rycke, which is no longer actively maintained. It is not a drop-in replacement for Jace.NET, but you should be able to switch to sonic with little effort.
sonic is considerably faster than Jace.NET (see benchmarks below). It contains numerous bugfixes and a lot of maintenance work over the latest Jace.NET release (1.0.0). Many of them were originally suggested and developed by the community for Jace.NET, but never merged due to the dormant state of the project. See the changelog for details and a complete list.
Installation
sonic is available via nuget:
dotnet add package Adletec.Sonic --version 1.6.0
Usage
Evaluating an Expression
Directly Evaluate an Expression
The easiest way to evaluate an expression is to use the Evaluate()-method of the Evaluator:
var expression = "var1*var2";
var variables = new Dictionary<string, double>();
variables.Add("var1", 2.5);
variables.Add("var2", 3.4);
var engine = Evaluator.CreateWithDefaults();
double result = engine.Evaluate(expression, variables);
Create a Delegate for an Expression
sonic can also create a delegate (Func) from your expression which will take the variable dictionary as argument:
var expression = "var1+2/(3*otherVariable)";
var engine = Evaluator.CreateWithDefaults();
Func<Dictionary<string, double>, double> evaluate = engine.CreateDelegate(expression);
var variables = new Dictionary<string, double>();
variables.Add("var1", 2);
variables.Add("otherVariable", 4.2);
double result = evaluate(variables);
If you intend to evaluate the same expression repeatedly with different variables, you should use this method. It will avoid the overhead of retrieving the delegate from the cache, based on the expression string. On the other hand, there is no performance benefit in using this method if you are only evaluating the expression once.
Handling Spaces and Special Characters
sonic expects expressions to contain alpha-numeric characters and mathematical operators only.
However, it's possible to use single quotes (') to wrap any symbol or function name, which will allow you to use arbitrary characters, including spaces, emojis, and even mathematical operators as part of your token name.
[!NOTE] There is no escaping mechanism for single quotes, i.e. you can't use single quotes in your token names. Apart from that, everything inside the single quotes will be treated as a black box, so every valid string is a valid token name.
Be aware that the quotation is not part of the token name, so sin('x') and sin(x) are equivalent. This also means that it's only necessary to use single quotes in the expression, not in the variable dictionary.
var expression = "sin('x') + 'my variable'";
var variables = new Dictionary<string, double>();
variables.Add("x", 0);
variables.Add("my variable", 3.4);
var engine = Evaluator.CreateWithDefaults();
double result = engine.Evaluate(expression, variables); // 3.4
[!CAUTION] You might want to use this feature to allow the usage of arbitrary token names from external sources in your application, e.g. from user input. Be aware that sonic won't sanitize the input or token names in any way. This means that defining an expression with user defined token names (e.g.
var expression = $"1234 + '{tokenFromUserInput}'";) will allow the user to inject arbitrary code into your expression.In other words, don't use user input as token names, if you don't want them to manipulate your expression.
Operators
sonic supports a wide range of operators, including simple Boolean logic:
| Operator | Operation | Usage | Parameters |
|----------|------------|--------------------------|-----------------------------------------|
| + | a + b | Addition | a: first summand, b: second summand |
| - | a - b | Subtraction | a: minuend, b: subtrahend |
| * | a * b | Multiplication | a: first factor, b: second factor |
| / | a / b | Division | a: dividend, b: divisor |
| % | a % b | Modulo | a: dividend, b: divisor |
| ^ | a ^ b | Exponentiation | a: base, b: exponent |
| == | a == b | Equality | a: first value, b: second value |
| != | a != b | Inequality | a: first value, b: second value |
| < | a < b | Less than | a: first value, b: second value |
| <= | a <= b | Less than or equal to | a: first value, b: second value |
| > | a > b | Greater than | a: first value, b: second value |
| >= | a >= b | Greater than or equal to | a: first value, b: second value |
| && | a && b | Logical AND | a: first value, b: second value |
| \|\| | a \|\| b | Logical OR | a: first value, b: second value |
Boolean operators will return 1 if the condition is true and 0 if it is false. As input parameters, they accept
any numeric value, which will be interpreted as true if it is not 0 and false if it is 0.
var expression = "var1 > var2 && var3 < 5";
var variables = new Dictionary<string, double>();
variables.Add("var1", 1);
variables.Add("var2", 2);
variables.Add("var3", 3);
var engine = Evaluator.CreateWithDefaults();
double result = engine.Evaluate
