ParserLibrary 3.1.5
dotnet add package ParserLibrary --version 3.1.5
NuGet\Install-Package ParserLibrary -Version 3.1.5
<PackageReference Include="ParserLibrary" Version="3.1.5" />
<PackageVersion Include="ParserLibrary" Version="3.1.5" />
<PackageReference Include="ParserLibrary" />
paket add ParserLibrary --version 3.1.5
#r "nuget: ParserLibrary, 3.1.5"
#:package ParserLibrary@3.1.5
#addin nuget:?package=ParserLibrary&version=3.1.5
#tool nuget:?package=ParserLibrary&version=3.1.5
ParserLibrary
No Other Expression Parser, Ever
About
I wanted to create my own custom terminal that supported interactive commands with expressions. Other expression builders I found only used numbers as the basic entities, which I didn’t want—this felt too common. I wanted variables that could represent chords, vectors, matrices, and, of course, numbers. The only way to build an optimized version of what I wanted, was to build an expression builder that inherently support custom types. Naturally, the ability to handle numerical values was needed as a starting point.
The library is frequently updated, so please check back for newer versions and the most recent README.
The library is based on modern programming tools and can be highly customized. Its basic features are:
- Default support for:
- Double arithmetic via the
DefaultParser
- Complex arithmetic via the
ComplexParser
- Vector arithmetic via the
Vector3Parser
- Double arithmetic via the
- Logger customization (typically via the
appsettings.json
). - Full control of unary and binary operators via configuration files (typically
appsettings.json
). - Support for custom data types and/or combination of custom data types with standard data types (such as
int
,double
). - Support for custom functions with arbitrary number of arguments. Each argument may be a custom type.
The library is built with modern tools:
- Use of .NET Generic Host (i.e Dependency Inversion/Injection principles, Logging, Configuration) (see NET Generic Host for more). All derived Parsers are typically singletons.
- Support for custom loggers (Serilog is implemented by default)
There are 2 main classes: the Tokenizer
and the Parser
. Both of them are base classes and adapt to the corresponding interfaces ITokenizer
and IParser
. Let's uncover all the potential by giving examples with incrementally added functionality.
Installation
Via tha Package Manager:
Install-Package ParserLibrary
Via the .NET CLI
dotnet add package ParserLibrary
Namespaces
At least the first 2 namespaces below, should be used in order to compile most of the following examples.
//use at least these 2 namespaces
using ParserLibrary;
using ParserLibrary.Parsers;
using ParserLibrary.Parsers.Common;
using ParserLibrary.Tokenizers;
using ParserLibrary.ExpressionTree;
Simple Parser Examples
DefaultParser
examples
//This is a simple expression, which uses variables and literals of type double, and the DefaultParser.
double result = (double)ParserApp.Evaluate( "-5.0+2*a", new() { { "a", 5.0 } });
Console.WriteLine(result); //5
//2 variables example (spaces are obviously ignored)
double result2 = (double)ParserApp.Evaluate("-a + 500 * b + 2^3", new() { { "a", 5 }, { "b", 1 } });
Console.WriteLine(result2); //503
All examples below are equivalent; the first and the second examples explicitly retrieve the DefaultParser
instance, which contains default arithmetic functionality.
//The example below uses explicitly the DefaultParser. Use one of the alternatives below:
//v1
var app = ParserApp.GetCommonsApp(); //returns an IHost with the common parsers registered
var parser = app.Services.GetParser("Default");
double result = (double)parser.Evaluate("-5.0+2*a", new() { { "a", 5.0 } });
//v2
var parser = ParserApp.GetDefaultParser(); //returns an IHost with the common parsers registered
double result = (double)parser.Evaluate("-5.0+2*a", new() { { "a", 5.0 } });
//v3
double result = (double)ParserApp.Evaluate("-5.0+2*a", new() { { "a", 5.0 } });
Let's use some functions already defined in the DefaultParser
:
double result3 = (double)ParserApp.Evaluate("cosd(ph)^2+sind(ph)^2", new() { { "ph", 45 } });
Console.WriteLine(result3); // 1.0000000000000002
...and some constants used in the DefaultParser
:
Console.WriteLine(ParserApp.Evaluate("5+2*cos(pi)+3*ln(e)")); //will return 5 - 2 + 3 -> 6
DefaultParser
examples #2 (custom functions)
That was the boring stuff, let's start adding some custom functionality. Let's add a custom function add3
that takes 3 arguments. For this purpose, we create a new subclass of DefaultParser
. Note that we can add custom logging via dependency injection (some more examples will follow on this). For the moment, ignore the constructor. We assume that the add3
functions sums its 3 arguments with a specific weight.
public class SimpleFunctionParser(ILogger<ParserBase> logger, IOptions<TokenizerOptions> options) : DefaultParser(logger, options)
{
protected override object? EvaluateFunction(string functionName, object?[] args)
{
double[] d = GetDoubleFunctionArguments(args);
return functionName switch
{
"add3" => d[0] + 2 * d[1] + 3 * d[2],
//for all other functions use the base class stuff (DefaultParser)
_ => base.EvaluateFunction(functionName, args)
};
}
}
Let's use our first customized Parser
. There are many ways to retrieve the parser depending on the project needs. See some variants below:
//v1
var app = ParserApp.GetParserApp<SimpleFunctionParser>();
var parser = app.GetParser();
// will return 8 + (5 + 2 * 3 + 3 * 3.0) i.e -> 28
double result = (double)parser.Evaluate("8 + add3(5.0,g,3.0)", new() { { "g", 3 } });
//v2 (assuming you want to explicitly configure the host)
var app = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddParser<SimpleFunctionParser>(context);
}).Build();
var parser = app.GetParser();
//v3 (assuming you want to add multiple parsers, a keyed version is suitable)
var app = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddParser<SimpleFunctionParser>("simple", TokenizerOptions.Default);
services.AddParser<ComplexParser>("complex", TokenizerOptions.Default);
}).Build();
var parser = app.GetParser("simple");
...
Single type parsing
If we want to parse an expression that deals with a single data type, then we can avoid the use of creating a custom parser, using the generic Evaluate<T>
function.
In the example below, we assume that the expression contains only int
data types. For this case the CoreParser
can be used.
The CoreParser
can be created in many ways:
//v1
IParser parser = ParserApp.GetCoreParser();
//v2 (explicitly creating the parser with a new host)
IHost app = ParserApp.GetParserApp<CoreParser>();
IParser parser = app.GetParser();
//v3 (use the 'commons' app (host) which includes common parsers: "Core", "Default" or "Double", "Complex", "Vector3")
IHost app = ParserApp.GetCommonsApp();
IParser parser = app.GetParser("Core");
After retrieving the parser based on preference, the Evaluate(T)
method is used as shown below. In this example, we define custom functions f10
and f2
, which multiply their argument by 10 and 2 respectively. We also define the operators +
, *
and ^
(power). The variables a
and asd
are also defined. The expression is evaluated to an integer value.
//we use the core Parser here
IParser parser = ParserApp.GetCoreParser();
int result = parser.Evaluate<int>( //returns 860
"a+f10(8+5) + f2(321+asd*2^2)",
(s) => int.Parse(s),
variables: new () {
{ "a", 8 },
{ "asd", 10 } },
binaryOperators: new () {
{ "+",(v1,v2)=>v1+v2} ,
{ "*", (v1, v2) => v1 * v2 },
{ "^",(v1,v2)=>(int)Math.Pow(v1,v2)} },
funcs1Arg:
new () {
{ "f10", (v) => 10 * v } ,
{ "f2", (v) => 2 * v }}
);
From the declaration of the function below, we can see that the Evaluate
function supports functions from up to 3 arguments and the definition of custom operators. As shown in the example above, it is best to use the named parameters syntax.
V Evaluate<V>(
string expression,
Func<string, V> literalParser = null,
Dictionary<string, V> variables = null,
Dictionary<string, Func<V, V, V>> binaryOperators = null,
Dictionary<string, Func<V, V>> unaryOperators = null,
Dictionary<string, Func<V, V>>? funcs1Arg = null,
Dictionary<string, Func<V, V, V>>? funcs2Arg = null,
Dictionary<string, Func<V, V, V, V>>? funcs3Arg = null
);
Custom Parsers
Any Parser
that uses custom types should inherit the CoreParser
base class.
Each custom parser should override the members:
Constants
: if there is at least one "constant" such aspi
, which should be defined by default.EvaluateLiteral
: if there is at least one literal value such as0.421
. In most cases a simple parse function can be called for adouble
orint
.EvaluateUnaryOperator
: if there is at least one unary operatorEvaluateOperator
: if there is at least one binary operatorEvaluateFunction
: if there is at least one function. It is best to understand how to override these functions in the example implementations below.
Custom parser examples #1: ComplexParser
Another ready to use Parser
is the ComplexParser
for complex arithmetic. The application of the Parser
for Complex
numbers is a first application of a custom data type (i.e. other that double
). Let's see the implementation of the ComplexParser
to clarify how a generic custom parser is implemented:
using System.Numerics;
namespace ParserLibrary.Parsers;
public class ComplexParser(ILogger<Parser> logger, IOptions<TokenizerOptions> options) : CoreParser(logger, options)
{
public override Dictionary<string, object?> Constants =>
new(_options.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase)
{
{ "i", Complex.ImaginaryOne },
{ "j", Complex.ImaginaryOne },
{ "pi", new Complex(Math.PI, 0) },
{ "e", new Complex(Math.E, 0) }
};
protected override object EvaluateLiteral(string s) =>
double.Parse(s, CultureInfo.InvariantCulture);
#region Auxiliary functions to get operands
private static Complex GetComplex(object? value)
{
if (value is null) return Complex.Zero;
if (value is double) return new Complex(Convert.ToDouble(value), 0);
if (value is not Complex) return Complex.Zero;
return (Complex)value;
}
public static (Complex Left, Complex Right) GetComplexBinaryOperands(object? leftOperand, object? rightOperand) => (
Left: GetComplex(leftOperand),
Right: GetComplex(rightOperand)
);
public static Complex GetComplexUnaryOperand(object? operand) => GetComplex(operand);
public static Complex[] GetComplexFunctionArguments(object?[] args) =>
[.. args.Select(GetComplex)];
#endregion
protected override object? EvaluateUnaryOperator(string operatorName, object? operand)
{
Complex op = GetComplexUnaryOperand(operand);
return operatorName switch
{
"-" => -op,
"+" => op,
_ => base.EvaluateUnaryOperator(operatorName, operand),
};
}
protected override object? EvaluateOperator(string operatorName, object? leftOperand, object? rightOperand)
{
var (Left, Right) = GetComplexBinaryOperands( leftOperand,rightOperand);
return operatorName switch
{
"+" => Complex.Add(Left, Right),
"-" => Left - Right,
"*" => Left * Right,
"/" => Left / Right,
"^" => Complex.Pow(Left, Right),
_ => base.EvaluateOperator(operatorName, leftOperand,rightOperand),
};
}
protected override object? EvaluateFunction(string functionName, object?[] args)
{
Complex[] a = GetComplexFunctionArguments(args);
const double TORAD = Math.PI / 180.0, TODEG = 180.0 * Math.PI;
return functionName switch
{
"abs" => Complex.Abs(a[0]),
"acos" => Complex.Acos(a[0]),
"acosd" => Complex.Acos(a[0]) * TODEG,
"asin" => Complex.Asin(a[0]),
"asind" => Complex.Asin(a[0]) * TODEG,
"atan" => Complex.Atan(a[0]),
"atand" => Complex.Atan(a[0]) * TODEG,
"cos" => Complex.Cos(a[0]),
"cosd" => Complex.Cos(a[0] * TORAD),
"cosh" => Complex.Cosh(a[0]),
"exp" => Complex.Exp(a[0]),
"log" or "ln" => Complex.Log(a[0]),
"log10" => Complex.Log10(a[0]),
"log2" => Complex.Log(a[0]) / Complex.Log(2),
"logn" => Complex.Log(a[0]) / Complex.Log(a[1]),
"pow" => Complex.Pow(a[0], a[1]),
"round" => new Complex(Math.Round(a[0].Real, (int)a[1].Real), Math.Round(a[0].Imaginary, (int)a[1].Real)),
"sin" => Complex.Sin(a[0]),
"sind" => Complex.Sin(a[0] * TORAD),
"sinh" => Complex.Sinh(a[0]),
"sqr" or "sqrt" => Complex.Sqrt(a[0]),
"tan" => Complex.Tan(a[0]),
"tand" => Complex.Tan(a[0] * TORAD),
"tanh" => Complex.Tanh(a[0]),
_ => base.EvaluateFunction(functionName, args),
};
}
}
Below is an example of usage of the ComplexParser
:
using System.Numerics; //needed if we want to further use the result
...
var cparser = ParserApp.GetComplexParser();
//unless we override the i or j variables, both are considered to correspond to the imaginary unit
//NOTE: because i is used as a variable (internally), the syntax for the imaginary part should be 3*i, NOT 3i
Complex result = (Complex)cparser.Evaluate("(1+3*i)/(2-3*i)");
Console.WriteLine(result); // (-0.5384615384615385, 0.6923076923076924)
//another one with a variable (should give the same result)
Complex result2 = (Complex)cparser.Evaluate("(1+3*i)/b", new() { { "b", new Complex(2,-3)} });
Console.WriteLine(result2); //same result
//and something more "complex", using nested functions: note that the complex number is returned as a string in the form (real, imaginary)
Console.WriteLine(cparser.Evaluate("cos((1+i)/(8+i))")); //(0.9961783779071353, -0.014892390041785901)
Console.WriteLine(cparser.Evaluate("round(cos((1+i)/(8+i)),4)")); //(0.9962, -0.0149)
Console.WriteLine(cparser.Evaluate("round(exp(i*pi),8)")); //(-1, 0) (Euler is correct!)
Custom parser examples #2: Vector3Parser
Vector3Parser
is the corresponding parser for vector arithmetic. The Vector3
is also included in the System.Numerics
namespace. The implementation of the Vector3Parser
is similar to the implementation of the ComplexParser
. The same methods from the Parser
base class are overriden.
namespace ParserLibrary.Parsers;
using ParserLibrary.Tokenizers;
using System.Numerics;
public class Vector3Parser(ILogger<Parser> logger, IOptions<TokenizerOptions> options) : CoreParser(logger, options)
{
public override Dictionary<string, object?> Constants =>
new(_options.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase)
{
{ "pi", DoubleToVector3((float)Math.PI) },
{ "e", DoubleToVector3((float)Math.E) },
{ "ux", Vector3.UnitX },
{ "uy", Vector3.UnitY },
{ "uz", Vector3.UnitZ }
};
protected override object EvaluateLiteral(string s) =>
float.Parse(s, CultureInfo.InvariantCulture);
#region Auxiliary functions to get operands
public static Vector3 DoubleToVector3(object arg)
=> new(Convert.ToSingle(arg), Convert.ToSingle(arg), Convert.ToSingle(arg));
public static bool IsNumeric(object arg) =>
arg is double || arg is int || arg is float;
public static Vector3 GetVector3(object? arg)
{
if (arg is null) return Vector3.Zero;
if (IsNumeric(arg)) return DoubleToVector3(arg);
if (arg is Vector3 v) return v;
return Vector3.Zero;
}
public static Vector3 GetVector3UnaryOperand(object? operand) =>
GetVector3(operand);
public static (Vector3 Left, Vector3 Right) GetVector3BinaryOperands(object? leftOperand, object? rightOperand) => (
Left: GetVector3(leftOperand),
Right: GetVector3(rightOperand)
);
public static Vector3[] GetVector3FunctionArguments(object?[] args) =>
[.. args.Select(GetVector3)];
#endregion
protected override object? EvaluateUnaryOperator(string operatorName, object? operand)
{
Vector3 op = GetVector3UnaryOperand(operand);
return operatorName switch
{
"-" => -op,
"+" => op,
"!" => Vector3.Normalize(op),
_ => base.EvaluateUnaryOperator(operatorName, operand)
};
}
protected override object? EvaluateOperator(string operatorName, object? leftOperand, object? rightOperand)
{
(Vector3 left, Vector3 right) = GetVector3BinaryOperands(leftOperand,rightOperand);
return operatorName switch
{
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => left / right,
"^" => Vector3.Cross(left, right),
"@" => Vector3.Dot(left, right),
_ => base.EvaluateOperator(operatorName, leftOperand,rightOperand)
};
}
protected override object? EvaluateFunction(string functionName, object?[] args)
{
Vector3[] a = GetVector3FunctionArguments(args);
return functionName switch
{
"abs" => Vector3.Abs(a[0]),
"cross" => Vector3.Cross(a[0], a[1]),
"dot" => Vector3.Dot(a[0], a[1]),
"dist" => Vector3.Distance(a[0], a[1]),
"dist2" => Vector3.DistanceSquared(a[0], a[1]),
"lerp" => Vector3.Lerp(a[0], a[1], a[2].X),
"length" => a[0].Length(),
"length2" => a[0].LengthSquared(),
"norm" => Vector3.Normalize(a[0]),
"sqr" or "sqrt" => Vector3.SquareRoot(a[0]),
"round" => new Vector3(
(float)Math.Round(a[0].X, (int)a[1].X),
(float)Math.Round(a[0].Y, (int)a[1].X),
(float)Math.Round(a[0].Z, (int)a[1].X)),
_ => base.EvaluateFunction(functionName, args),
};
}
}
Let's see some example usage too:
using System.Numerics; //needed if we want to further use the result
...
var vparser = ParserApp.GetVector3Parser();
Vector3 v1 = new Vector3(1, 4, 2),
v2 = new Vector3(2, -2, 0);
Console.WriteLine(vparser.Evaluate("!(v1+3*v2)", //! means normalize vector
new() { { "v1", v1 }, { "v2", v2 } })); //<0.92717266. -0.26490647. 0.26490647>
Console.WriteLine(vparser.Evaluate("10 + 3 * v1^v2", // ^ means cross product
new() { { "v1", v1 }, { "v2", v2 } })); //<22. 22. -20>
Console.WriteLine(vparser.Evaluate("v1@v2", // @ means dot product
new() { { "v1", v1 }, { "v2", v2 } })); //-6
Console.WriteLine(vparser.Evaluate("lerp(v1, v2, 0.5)", // lerp (linear combination of vectors)
new() { { "v1", v1 }, { "v2", v2 } })); //<1.5, 1. 1>
Console.WriteLine(vparser.Evaluate("6*ux -12*uy + 14*uz")); //<6. -12. 14>
Custom parser examples #3: ItemParser
and the ItemStatefulParser
Let's assume that we have a class named Item
, which we want to interact with integer numbers and with other Item
objects:
public class Item
{
public string Name { get; set; }
public int Value { get; set; } = 0;
//we define a custom operator for the type to simplify the evaluateoperator example later
//this is not 100% needed, but it keeps the code in the custom parser simpler
public static Item operator +(int v1, Item v2) =>
new Item { Name = v2.Name , Value = v2.Value + v1 };
public static Item operator +(Item v2, int v1) =>
new Item { Name = v2.Name, Value = v2.Value + v1 };
public static Item operator +(Item v1, Item v2) =>
new Item { Name = $"{v1.Name} {v2.Name}", Value = v2.Value + v1.Value };
public override string ToString() => $"{Name} {Value}";
}
A custom parser that uses custom types derives from the CoreParser
class. Because the CoreParser
class does not assume any type in advance, we should override the EvaluateLiteral
function which is used to parse the integer numbers in the string. In the following example we define the +
operator, which can take an Item
object or an int
for its operands. We also define the add
function, which assumes that the first argument is an Item
and the second argument is an int
. In practice, the Function syntax is usually stricter regarding the type of the arguments, so it is easier to write its implementation:
public class ItemParser(ILogger<CoreParser> logger, IOptions<TokenizerOptions> options) : CoreParser(logger, options)
//we assume that literals are integer numbers only
protected override object EvaluateLiteral(string s) => int.Parse(s);
protected override object? EvaluateOperator(string operatorName, object? leftOperand, object? rightOperand)
{
if (operatorName == "+")
{
_logger.LogDebug("Adding with + operator ${left} and ${right}", leftOperand, rightOperand);
//we manage all combinations of Item/Item, Item/int, int/Item combinations here
if (leftOperand is Item left && rightOperand is Item right)
return left + right;
return leftOperand is Item ?
(Item)leftOperand + (int)rightOperand! : (int)leftOperand! + (Item)rightOperand!;
}
return base.EvaluateOperator(operatorName, leftOperand, rightOperand);
}
protected override object? EvaluateFunction(string functionName, object?[] args)
{
if (args[0] is not Item || args[1] is not int)
{
_logger.LogError("Invalid arguments for function {FunctionName}: {Args}", functionName, args);
throw new ArgumentException($"Invalid arguments for function {functionName}");
}
return functionName switch
{
"add" => (Item)args[0]! + (int)args[1]!,
_ => base.EvaluateFunction(functionName, args)
};
}
}
Now we can use the ItemParser
for parsing our custom expression. To create the parser we can use many alternatives:
//v1 (implicitly create host too)
var parser = ParserApp.GetParser<ItemParser>();
//v2 (explicitly create host)
var parser = ParserApp.GetParserApp<ItemParser>().GetParser();
//v3 (explicitly add to existing host)
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddParser<ItemParser>(context);
...
}).Build();
var parser = host.GetParser();
//v4 (keyed version, if multiple parsers are used)
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddParser<ItemParser>("item", TokenizerOptions.Default);
}).Build();
var parser = host.GetParser("item");
After retrieving the parser, we can evaluate an expression as shown below:
var parser = ParserApp.GetParser<ItemParser>();
Item result = (Item)parser.Evaluate("a + add(b,4) + 5",
new() {
{"a", new Item { Name="foo", Value = 3} },
{"b", new Item { Name="bar"} }
});
Console.WriteLine(result); // foo bar 12
StatefulParser
When you need:
- expression optimization
- validation before evaluation,
use a stateful parser variant. A stateful parser derives from
StatefulParserBase
(which itself supplies all core functionality plus mutableExpression
/Variables
).
StatefulParsers are Transient by default contrary to the common Parsers, which are Singletons and do not keep state.
Because they are stateful, you can keep the expression (via the Expression
property) and change the Variables
before re-evaluation.
The StatefulParser also has a Validate
method, which can be used to check if the expression is valid without actually evaluating it. This can be useful in interactive applications.
The method returns a collection of all the validation errors found.
Minimal example (mirrors the logic of the stateless ItemParser
):
public class ItemStatefulParser(
ILogger<ItemStatefulParser> logger,
IOptions<TokenizerOptions> options,
string expression,
Dictionary<string, object?>? variables = null) : StatefulParser(logger, options, expression, variables)
{
//we assume that literals are integer numbers only
protected override object EvaluateLiteral(string s) => int.Parse(s);
protected override object? EvaluateOperator(string operatorName, object? leftOperand, object? rightOperand)
{
if (operatorName == "+")
{
_logger.LogDebug("Adding with + operator ${left} and ${right}", leftOperand, rightOperand);
//we manage all combinations of Item/Item, Item/int, int/Item combinations here
if (leftOperand is Item left && rightOperand is Item right)
return left + right;
return leftOperand is Item ?
(Item)leftOperand + (int)rightOperand! : (int)leftOperand! + (Item)rightOperand!;
}
return base.EvaluateOperator(operatorName, leftOperand, rightOperand);
}
protected override object? EvaluateFunction(string functionName, object?[] args)
{
string actualFunctionName = _options.CaseSensitive ? functionName : functionName.ToLower();
return actualFunctionName switch
{
"add" => (Item)args[0]! + (int)args[1]!,
_ => base.EvaluateFunction(functionName, args)
};
}
}
Initializing a stateful parser
There are many ways to create a statefule parser in a similar manner to the (stateless) Parser
variants.
//v1 (implicitly create host too)
var parser = ParserApp.GetStatefulParser<ItemStatefulParser>();
//v2 (explicitly create host)
var parser = ParserApp.GetStatefulParserApp<ItemStatefulParser>().GetStatefulParser();
//v3 (explicitly add to existing host)
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddStatefulParser<ItemStatefulParser>(context);
...
}).Build();
var parser = host.GetStatefulParser();
//v4 (keyed version, if multiple parsers are used)
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddStatefulParser<ItemStatefulParser>("item", TokenizerOptions.Default);
}).Build();
var parser = host.GetStatefulParser("item");
We can then evaluate the expression the same way.
Item result = (Item)parser.Evaluate("a + add(b,4) + 5",
new() {
{"a", new Item { Name="foo", Value = 3} },
{"b", new Item { Name="bar"} }
});
Console.WriteLine(result); // foo bar 12
Expression Tree
The library can build an expression tree for any parsed expression.
APIs involved (namespace ParserLibrary.ExpressionTree
):
Tree<T>
(for the parser you will useTree<Token>
)NodeBase
(base class of the internal node type used by the tree)- Printing extensions:
NodeBasePrintExtensions
(horizontal),NodeBasePrintExtensionsVertical
(vertical) TreeOptimizer<T>
(optional reordering / optimization)- Traversal helpers via
Tree<Token>.GetPostfixTokens()
andTree<Token>.GetInfixTokens()
- Cloning via
Tree<Token>.DeepClone()
Basic usage:
using ParserLibrary;
using ParserLibrary.Parsers.Interfaces;
using ParserLibrary.ExpressionTree;
using ParserLibrary.Tokenizers; // Token
using System;
// Acquire a parser (double arithmetic)
var parser = ParserApp.GetDefaultParser();
// Build the expression tree (variables only needed for evaluation, not structure)
var tree = parser.GetExpressionTree("3 + 5 * (2 - 8) / a");
// Vertical print (recommended)
tree.Print2(); // or: tree.Root.PrintVerticalTree(leftOffset: 0, gap: 0)
// Horizontal print (legacy styles)
tree.Root.PrintWithDashes();
// tree.Root.PrintWithSlashes();
// Metrics
int height = tree.GetHeight();
int count = tree.Count;
int leaves = tree.GetLeafNodesCount();
// Traversals
List<Token> postfix = tree.GetPostfixTokens(); // Reverse Polish
List<Token> infix = tree.GetInfixTokens(); // In-order tokens
// Clone + optimize (commutative (+, *) regrouping heuristic)
var optimizer = new TreeOptimizer<Token>();
var optimized = optimizer.OptimizeForDataTypes(tree, new Dictionary<string, Type> { { "a", typeof(int) } });
// Show both
Console.WriteLine("Original:");
tree.Print2();
Console.WriteLine("Optimized:");
optimized.Print2();
Notes:
GetExpressionTree
is purely structural; variable values are only needed when you evaluate, not when you build the tree.Tree<Token>.GetPostfixTokens()
reproduces the postfix sequence derived from the tree (can be compared with the direct tokenizer result).TreeOptimizer<Token>
currently focuses on commutative operator grouping (+
,*
) and reordering.- Printing helpers write directly to the console;
ToVerticalTreeString()
(on the root viaroot.ToVerticalTreeString()
) returns a string if you need to log or snapshot the layout.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. 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. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- FluentValidation (>= 12.0.0)
- Microsoft.Extensions.Hosting (>= 9.0.8)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.8)
- OneOf (>= 3.0.271)
- Serilog.AspNetCore (>= 9.0.0)
-
net9.0
- FluentValidation (>= 12.0.0)
- Microsoft.Extensions.Hosting (>= 9.0.8)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.8)
- OneOf (>= 3.0.271)
- Serilog.AspNetCore (>= 9.0.0)
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 | |
---|---|---|---|
3.1.5 | 21 | 8/25/2025 | |
3.1.4 | 18 | 8/25/2025 | |
3.1.2 | 16 | 8/24/2025 | |
3.1.1 | 18 | 8/24/2025 | |
3.1.0 | 16 | 8/24/2025 | |
3.0.1 | 16 | 8/24/2025 | |
3.0.0 | 19 | 8/24/2025 | |
2.6.2 | 20 | 8/23/2025 | |
2.6.1 | 87 | 8/20/2025 | |
2.6.0 | 107 | 8/19/2025 | |
2.5.4 | 122 | 8/14/2025 | |
2.5.3 | 111 | 8/11/2025 | |
2.5.2 | 114 | 8/11/2025 | |
2.5.1 | 117 | 8/11/2025 | |
2.5.0 | 113 | 8/11/2025 | |
2.4.3 | 116 | 8/11/2025 | |
2.4.2 | 115 | 8/10/2025 | |
2.4.1 | 113 | 8/10/2025 | |
2.4.0 | 177 | 8/4/2025 | |
2.3.3 | 173 | 8/4/2025 | |
2.3.2 | 128 | 6/30/2025 | |
2.3.1 | 128 | 6/30/2025 | |
2.3.0 | 125 | 6/29/2025 | |
2.2.0 | 94 | 6/29/2025 | |
2.1.4 | 228 | 6/13/2025 | |
2.1.3 | 224 | 6/13/2025 | |
2.1.1 | 238 | 6/13/2025 | |
2.1.0 | 253 | 6/13/2025 | |
2.0.4 | 278 | 6/13/2025 | |
2.0.3 | 272 | 6/12/2025 | |
2.0.2 | 279 | 6/12/2025 | |
2.0.1 | 276 | 6/12/2025 | |
1.8.2 | 276 | 6/12/2025 | |
1.8.1 | 277 | 6/12/2025 | |
1.8.0 | 147 | 6/1/2025 | |
1.7.2 | 106 | 5/31/2025 | |
1.7.1 | 97 | 5/31/2025 | |
1.7.0 | 93 | 5/31/2025 | |
1.6.1 | 157 | 3/26/2025 | |
1.6.0 | 135 | 3/26/2025 | |
1.5.0 | 125 | 12/8/2024 | |
1.4.12 | 334 | 3/1/2024 | |
1.4.11 | 219 | 6/26/2023 | |
1.4.10 | 176 | 6/26/2023 | |
1.4.9 | 176 | 6/25/2023 | |
1.4.8 | 183 | 6/23/2023 | |
1.4.7 | 167 | 6/19/2023 | |
1.4.6 | 195 | 6/16/2023 | |
1.4.5 | 444 | 8/20/2022 | |
1.4.4 | 432 | 8/19/2022 | |
1.4.3 | 469 | 8/5/2022 | |
1.4.0 | 443 | 8/2/2022 | |
1.3.3 | 455 | 7/25/2022 | |
1.3.2 | 452 | 7/24/2022 | |
1.3.1 | 459 | 7/18/2022 | |
1.2.1 | 474 | 7/17/2022 | |
1.1.1 | 489 | 7/17/2022 | |
1.1.0 | 481 | 7/17/2022 | |
1.0.15 | 537 | 7/17/2022 | |
1.0.12 | 696 | 7/17/2022 | |
1.0.11 | 665 | 7/17/2022 | |
1.0.10 | 650 | 7/17/2022 | |
1.0.9 | 662 | 7/17/2022 | |
1.0.8 | 664 | 7/17/2022 | |
1.0.7 | 652 | 7/17/2022 | |
1.0.6 | 800 | 7/16/2022 | |
1.0.5 | 833 | 7/15/2022 | |
1.0.4 | 823 | 7/11/2022 | |
1.0.3 | 855 | 7/11/2022 | |
1.0.2 | 823 | 7/11/2022 | |
1.0.1 | 820 | 7/10/2022 | |
1.0.0 | 683 | 7/10/2022 |