StirlingLabs.StringToExpression 23.1.1

Prefix Reserved
dotnet add package StirlingLabs.StringToExpression --version 23.1.1                
NuGet\Install-Package StirlingLabs.StringToExpression -Version 23.1.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="StirlingLabs.StringToExpression" Version="23.1.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add StirlingLabs.StringToExpression --version 23.1.1                
#r "nuget: StirlingLabs.StringToExpression, 23.1.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install StirlingLabs.StringToExpression as a Cake Addin
#addin nuget:?package=StirlingLabs.StringToExpression&version=23.1.1

// Install StirlingLabs.StringToExpression as a Cake Tool
#tool nuget:?package=StirlingLabs.StringToExpression&version=23.1.1                

StringToExpression

Integration Codacy Badge GitHub release (latest SemVer)

This is a fork of Alex Davies' StringToExpression library.

StringToExpression allows you to create methods that take strings and outputs .NET expressions. It is highly configurable allowing you to define your own language with your own syntax.

Available via NuGet as StirlingLabs.StringToExpression.

Arithmetic

A basic arithmetic language ArithmeticLanguage is provided for performing algebra. It can be used as is, or extended as desired.

var language = new ArithmeticLanguage();
Expression<Func<decimal>> expressionFunction = language.Parse("(4 - 2) * 5 + 9 / 3");
Func<decimal> function = expressionFunction.compile();

Assert.Equal(13, function());

OData filter

ODataFilterLanguage is provided as a lightweight way to parse OData filter expressions which are a nice way to pass generic filtering requirements into a WebAPI.

public async Task<IHttpActionResult> GetDoohickies([FromUri(Name = "$filter")] string filter = "name eq 'discount' and rating gt 18")
{
  var language = new ODataFilterLanguage()
  Expression<Func<Doohicky, bool>> predicate = language.Parse<Doohickey>(filter);
  
  //can either pass this expression into either IQueryable or IEnumerable where clauses
  return await DataContext.Doohickies.Where(predicte).ToListAsync();
}

StringToExpression has the advantage of being configurable; if the OData parser doesnt support methods you want, (or it supports methods you dont want) it is very easy to extend ODataFilterLanguage and modify the configuration

Custom languages

Languages are defined by a set of GrammarDefintions. These define both how the string is broken up into tokens as well as the behaviour of each token. There are many subclasses of GrammarDefinition that makes implementing standard language features very easy.

An example of a very simple arithmetic language is as follows

ListDelimiterDefinition delimeter;
BracketOpenDefinition openBracket, sqrt;
language = new Language(new [] {
    new OperandDefinition(
        name:"DECIMAL",
        regex: @"\-?\d+(\.\d+)?",
        expressionBuilder: x => Expression.Constant(decimal.Parse(x))),
    new BinaryOperatorDefinition(
        name:"ADD",
        regex: @"\+",
        orderOfPrecedence: 2,
        expressionBuilder: (left,right) => Expression.Add(left, right)),
    new BinaryOperatorDefinition(
        name:"SUB",
        regex: @"\-",
        orderOfPrecedence: 2,
        expressionBuilder: (left,right) => Expression.Subtract(left, right)),
    new BinaryOperatorDefinition(
        name:"MUL",
        regex: @"\*",
        orderOfPrecedence: 1, //multiply should be done before add/subtract
        expressionBuilder: (left,right) => Expression.Multiply(left, right)),
    new BinaryOperatorDefinition(
        name:"DIV",
        regex: @"\/",
        orderOfPrecedence: 1, //division should be done before add/subtract
        expressionBuilder: (left,right) => Expression.Divide(left, right)),
    sqrt = new FunctionCallDefinition(
        name:"FN_SQRT",
        regex: @"sqrt\(",
        argumentTypes: new[] {typeof(double) },
        expressionBuilder: (parameters) => {
            return Expression.Call(
                null,
                method:typeof(Math).GetMethod("Sqrt"),
                arguments: new [] { parameters[0] });
        }),
    openBracket = new BracketOpenDefinition(
        name: "OPEN_BRACKET",
        regex: @"\("),
    delimeter = new ListDelimiterDefinition(
        name: "COMMA",
        regex: ","),
    new BracketCloseDefinition(
        name: "CLOSE_BRACKET",
        regex: @"\)",
        bracketOpenDefinitions: new[] { openBracket, sqrt },
        listDelimeterDefinition: delimeter)
    new GrammarDefinition(name: "WHITESPACE", regex: @"\s+", ignore: true) //we dont want to process whitespace
  });

Some of the out of the box grammar definitions are detailed below

Name Description Properties
GrammarDefintion Base class for all definitions. Does not perform any functionality during the parsing. name - A name for this rule.<br />regex - the regular expression that will match for this token.
OperandDefinition Defines the smallest atomic piece in your language, used to represent items like numbers or strings. expressionBuilder - a function that when given the string matched from the regex it produces a .NET expression (usually an ConstantExpression).
BinaryOperatorDefintion An operation that takes parameters from the left and right of it. Often represents arithmetic operaitons (+, -, *, /) or equality checks (==, !=, < >) or boolean logic (and, or). orderOfPrecedence - determines when this function should run, lower numbers get run before higher numbers (allowing defining BEDMAS rules).<br />expressionBuilder - function that takes in two Expression (the left and right of the operator), and outputs a new expression combining them.
UnaryOperator An operation that takes a single parameter, used for operations such as not. orderOfPrecedence - determines when this function should run, lower numbers get run before higher numbers.<br />parameterPosition - whether the operand is to the left or right of the operator.
BracketOpenDefinition Defines an open bracket, functionally does not do much unless paired with a BracketCloseDefinition.
ListDelimeterDefinition The separator to use to denote lists within brackets (a , in most languages) functionally does not do much unless paired with a BracketCloseDefinition.
BracketCloseDefinition The expression between the brackets is evaluated first. bracketOpenDefinitions - list of definitions that would be treated as a start to the bracketing.<br />listDelimiterDefinition - definition of the ListDelimeterDefinition.
FunctionCallDefinition Defines a function that takes in a list of operands. Also acts as a bracket BracketOpenDefinition definition. argumentTypes - list of types that define the types expected and the number of arguments expecte.<br />expressionBuilder - takes an array of expressions and output a single expression.

If your language is more complicated than the provided GrammarDefinitions you are able to define your own by extending GrammarDefintion. You best read the Nuts and bolts section to determine the best way to implement your definition.

Error handling

All parsing exceptions extend ParseException. A ParseException will contain both a readable message and a StringSegment that represents what token(s) in the original input string caused the error.

The StringSegment allows pinpointing of issues such as where operands are missing, which function has too many parameters or where the unexpected character is. It provides a useful feedback for wherever you are getting your original strings from.

Nuts and bolts

Under the hood StringToExpression implements a shunting-yard algorithm.

The internal parsing state contains a Stack<Operand> and a Stack<Operator> that are built up during the parsing

  • Operand - Represent a .NET expression. This may be a simple ConstantExpression, or the root of a complicated Tree of BinaryExpressions
  • Operator - Is a function that can be run. Generally these functions when run will consume one or more operands and produce one operand. Such that run operators reduces the number of operands on the stack.

The parsing is done in roughly three steps

  1. Tokenize - The string is parsed through a tokenizer which uses the regular expressions defined in the GrammarDefinitions to break the strings into Tokens. A Token knows the GrammarDefinition that created it and the string value it represents.

  2. Apply GrammarDefinitions - All GrammarDefinition has an void Apply(Token token, ParseState state) method. We first read all the Tokens in sequentially and run each GrammarDefinition Apply method. The apply method can make any modifications to the state it wants, this can range from something simple like pushing an Operand on to the stack, to something more complicated like executing operands.

  3. Execute Operators -Once all the tokens are Applied we will start poping Operators off the stack and executing them. When an Operator executes its generally expected that it will consume one or more Operands and create one Operand. This way by the time we apply all the operators we should only have a single Operand on the stack, that is our result.

To customize you can make your own GrammarDefinition and implment the Apply method to meet your purposes.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
23.1.1 2,801 1/12/2023
23.1.0 407 1/12/2023
22.12.3 333 12/31/2022
22.12.2 304 12/30/2022
22.12.1 262 12/30/2022
22.12.0 292 12/29/2022