Warning
NCalcU is experimental and under active development, it will also not release until .NET 11 and C# 15 are officially released. Feedback is very welcome.
NCalcU is to NCalc what the Wii U is to the Wii. Basically the same name and ideas but different architecture.
It keeps the simplicity of evaluating expressions while redesigning the core around immutability, and extensibility.
For comparison, the original NCalc AST is mutable and the parser is not extensible.
The U stands for two core ideas:
- Unions: expressions, values, nodes, results, and errors are modeled using C# union types.
- (U)you: U can replace or extend the parser, evaluator, functions, operators, and execution behavior.
using NCalc;
var result = new Expression("1 + 2 * 3").Evaluate(); // 7L
var asyncResult = await new Expression("Sqrt(9) + 1").EvaluateAsync(); // 4dAn Expression parses lazily and retains its immutable AST. Reuse it with different
contexts. For one-off string or already-parsed AST evaluation, use
LogicalExpressionEvaluator.Default.Evaluate(...) or EvaluateAsync(...).
Following Fluid's options and function conventions,
configuration belongs in reusable options and per-execution data belongs in a context.
Functions are delegates receiving arguments and a context, returning ValueTask<object?>.
using NCalc.Evaluation;
var options = new EvaluationOptions();
options.Values["tax"] = 1.2m;
options.Functions["double"] = async (arguments, context) =>
{
arguments.RequireCount(1);
var value = await arguments.AtAsync(0);
return Convert.ToDecimal(value, context.Options.Culture) * 2;
};
options.Functions["answer"] = (arguments, context) =>
{
arguments.RequireCount(0);
return ValueTask.FromResult<object?>(42L);
};
var expression = new Expression("double(price) * tax", options);
var context = new EvaluationContext(options).SetValue("price", 10m);
var result = await expression.EvaluateAsync(context); // 24mvar options = new EvaluationOptions
{
Parser = new CustomOperatorParser()
};
options.BinaryOperators["@@"] = async (operands, context) =>
{
var left = Convert.ToDecimal(await operands.AtAsync(0), context.Options.Culture);
var right = Convert.ToDecimal(await operands.AtAsync(1), context.Options.Culture);
return left + right;
};
var result = new Expression("2 @@ 3", options).Evaluate(); // 5mvar result = await LogicalExpressionEvaluator.Default.TryEvaluateAsync("1 / 0");
var message = result switch
{
LiteralValue value => $"Result: {value.Value}",
ExpressionError error => $"Error: {error.Message}"
};using NCalc;
using NCalc.Parser;
var result = LogicalExpressionParser.Default.Parse("price * quantity ?? 0");
if (result is LogicalExpression expression)
{
// Inspect expression.Value or match the union cases.
}
else if (result is Parlot.ParseError error)
{
Console.WriteLine($"{error.Message} at {error.Position}");
}Override CreatePrimaryParser to add literal or unary syntax, or
CreateExpressionParser to add operator levels.
using Parlot.Fluent;
// Add a new primary form (a quoted identifier).
public sealed class QuotedIdentifierParser : DisallowSingleEqualsParser
{
protected override Parser<LogicalExpression> CreatePrimaryParser(
Parser<LogicalExpression> expression)
=> DelimitedIdentifier("'{", "}'").Or(base.CreatePrimaryParser(expression));
}
var parser = new QuotedIdentifierParser();
parser.Parse("f('{Example}', x + 1)");public sealed class CustomOperatorParser : LogicalExpressionParser
{
protected override Parser<LogicalExpression> CreateExpressionParser(
Parser<LogicalExpression> expression)
{
var standard = base.CreateExpressionParser(expression);
return BinaryLevel(standard, Terms.Text("@@"));
}
}