using System;
|
using System.Collections.Generic;
|
using System.Globalization;
|
using System.Linq;
|
using System.Text;
|
using Jace.Execution;
|
using Jace.Operations;
|
using Jace.Tokenizer;
|
using Jace.Util;
|
|
namespace Jace
|
{
|
public delegate TResult DynamicFunc<T, TResult>(params T[] values);
|
|
/// <summary>
|
/// The CalculationEngine class is the main class of Jace.NET to convert strings containing
|
/// mathematical formulas into .NET Delegates and to calculate the result.
|
/// It can be configured to run in a number of modes based on the constructor parameters choosen.
|
/// </summary>
|
public class CalculationEngine
|
{
|
private readonly IExecutor executor;
|
private readonly Optimizer optimizer;
|
private readonly CultureInfo cultureInfo;
|
private readonly MemoryCache<string, Func<IDictionary<string, double>, double>> executionFormulaCache;
|
private readonly bool cacheEnabled;
|
private readonly bool optimizerEnabled;
|
private readonly bool caseSensitive;
|
|
private readonly Random random;
|
|
/// <summary>
|
/// Creates a new instance of the <see cref="CalculationEngine"/> class with
|
/// default parameters.
|
/// </summary>
|
public CalculationEngine()
|
: this(new JaceOptions())
|
{
|
}
|
|
/// <summary>
|
/// Creates a new instance of the <see cref="CalculationEngine"/> class. The dynamic compiler
|
/// is used for formula execution and the optimizer and cache are enabled.
|
/// </summary>
|
/// <param name="cultureInfo">
|
/// The <see cref="CultureInfo"/> required for correctly reading floating poin numbers.
|
/// </param>
|
public CalculationEngine(CultureInfo cultureInfo)
|
: this(new JaceOptions() { CultureInfo = cultureInfo })
|
{
|
}
|
|
/// <summary>
|
/// Creates a new instance of the <see cref="CalculationEngine"/> class. The optimizer and
|
/// cache are enabled.
|
/// </summary>
|
/// <param name="cultureInfo">
|
/// The <see cref="CultureInfo"/> required for correctly reading floating poin numbers.
|
/// </param>
|
/// <param name="executionMode">The execution mode that must be used for formula execution.</param>
|
public CalculationEngine(CultureInfo cultureInfo, ExecutionMode executionMode)
|
: this (new JaceOptions() { CultureInfo = cultureInfo, ExecutionMode = executionMode })
|
{
|
}
|
|
/// <summary>
|
/// Creates a new instance of the <see cref="CalculationEngine"/> class.
|
/// </summary>
|
/// <param name="cultureInfo">
|
/// The <see cref="CultureInfo"/> required for correctly reading floating poin numbers.
|
/// </param>
|
/// <param name="executionMode">The execution mode that must be used for formula execution.</param>
|
/// <param name="cacheEnabled">Enable or disable caching of mathematical formulas.</param>
|
/// <param name="optimizerEnabled">Enable or disable optimizing of formulas.</param>
|
/// <param name="adjustVariableCaseEnabled">Enable or disable auto lowercasing of variables.</param>
|
[Obsolete]
|
public CalculationEngine(CultureInfo cultureInfo, ExecutionMode executionMode, bool cacheEnabled, bool optimizerEnabled, bool adjustVariableCaseEnabled)
|
: this(new JaceOptions() { CultureInfo = cultureInfo, ExecutionMode = executionMode, CacheEnabled = cacheEnabled, OptimizerEnabled = optimizerEnabled, CaseSensitive = !adjustVariableCaseEnabled })
|
{
|
}
|
|
/// <summary>
|
/// Creates a new instance of the <see cref="CalculationEngine"/> class.
|
/// </summary>
|
/// <param name="cultureInfo">
|
/// The <see cref="CultureInfo"/> required for correctly reading floating poin numbers.
|
/// </param>
|
/// <param name="executionMode">The execution mode that must be used for formula execution.</param>
|
/// <param name="cacheEnabled">Enable or disable caching of mathematical formulas.</param>
|
/// <param name="optimizerEnabled">Enable or disable optimizing of formulas.</param>
|
/// <param name="adjustVariableCaseEnabled">Enable or disable converting to lower case.</param>
|
/// <param name="defaultFunctions">Enable or disable the default functions.</param>
|
/// <param name="defaultConstants">Enable or disable the default constants.</param>
|
/// <param name="cacheMaximumSize">Configure the maximum cache size for mathematical formulas.</param>
|
/// <param name="cacheReductionSize">Configure the cache reduction size for mathematical formulas.</param>
|
[Obsolete]
|
public CalculationEngine(CultureInfo cultureInfo, ExecutionMode executionMode, bool cacheEnabled,
|
bool optimizerEnabled, bool adjustVariableCaseEnabled, bool defaultFunctions, bool defaultConstants, int cacheMaximumSize, int cacheReductionSize)
|
: this(new JaceOptions() { CultureInfo = cultureInfo, ExecutionMode = executionMode, CacheEnabled = cacheEnabled, OptimizerEnabled = optimizerEnabled,
|
CaseSensitive = !adjustVariableCaseEnabled, DefaultFunctions = defaultFunctions, DefaultConstants = defaultConstants,
|
CacheMaximumSize = cacheMaximumSize, CacheReductionSize = cacheReductionSize })
|
{
|
}
|
|
/// <summary>
|
/// Creates a new instance of the <see cref="CalculationEngine"/> class.
|
/// </summary>
|
/// <param name="options">The <see cref="JaceOptions"/> to configure the behaviour of the engine.</param>
|
public CalculationEngine(JaceOptions options)
|
{
|
this.executionFormulaCache = new MemoryCache<string, Func<IDictionary<string, double>, double>>(options.CacheMaximumSize, options.CacheReductionSize);
|
this.FunctionRegistry = new FunctionRegistry(false);
|
this.ConstantRegistry = new ConstantRegistry(false);
|
this.cultureInfo = options.CultureInfo;
|
this.cacheEnabled = options.CacheEnabled;
|
this.optimizerEnabled = options.OptimizerEnabled;
|
this.caseSensitive = options.CaseSensitive;
|
|
this.random = new Random();
|
|
if (options.ExecutionMode == ExecutionMode.Interpreted)
|
executor = new Interpreter(caseSensitive);
|
else if (options.ExecutionMode == ExecutionMode.Compiled)
|
executor = new DynamicCompiler(caseSensitive);
|
else
|
throw new ArgumentException(string.Format("Unsupported execution mode \"{0}\".", options.ExecutionMode),
|
"executionMode");
|
|
optimizer = new Optimizer(new Interpreter()); // We run the optimizer with the interpreter
|
|
// Register the default constants of Jace.NET into the constant registry
|
if (options.DefaultConstants)
|
RegisterDefaultConstants();
|
|
// Register the default functions of Jace.NET into the function registry
|
if (options.DefaultFunctions)
|
RegisterDefaultFunctions();
|
}
|
|
internal IFunctionRegistry FunctionRegistry { get; private set; }
|
|
internal IConstantRegistry ConstantRegistry { get; private set; }
|
|
public IEnumerable<FunctionInfo> Functions { get { return FunctionRegistry; } }
|
|
public IEnumerable<ConstantInfo> Constants { get { return ConstantRegistry; } }
|
|
public double Calculate(string formulaText)
|
{
|
return Calculate(formulaText, new Dictionary<string, double>());
|
}
|
|
public double Calculate(string formulaText, IDictionary<string, double> variables)
|
{
|
if (string.IsNullOrEmpty(formulaText))
|
throw new ArgumentNullException("formulaText");
|
|
if (variables == null)
|
throw new ArgumentNullException("variables");
|
|
if (!caseSensitive)
|
{
|
variables = EngineUtil.ConvertVariableNamesToLowerCase(variables);
|
}
|
VerifyVariableNames(variables);
|
|
// Add the reserved variables to the dictionary
|
foreach (ConstantInfo constant in ConstantRegistry)
|
variables.Add(constant.ConstantName, constant.Value);
|
|
if (IsInFormulaCache(formulaText, null, out var function))
|
{
|
return function(variables);
|
}
|
else
|
{
|
Operation operation = BuildAbstractSyntaxTree(formulaText, new ConstantRegistry(caseSensitive));
|
function = BuildFormula(formulaText, null, operation);
|
return function(variables);
|
}
|
}
|
|
public FormulaBuilder Formula(string formulaText)
|
{
|
if (string.IsNullOrEmpty(formulaText))
|
throw new ArgumentNullException("formulaText");
|
|
return new FormulaBuilder(formulaText, caseSensitive, this);
|
}
|
|
/// <summary>
|
/// Build a .NET func for the provided formula.
|
/// </summary>
|
/// <param name="formulaText">The formula that must be converted into a .NET func.</param>
|
/// <returns>A .NET func for the provided formula.</returns>
|
public Func<IDictionary<string, double>, double> Build(string formulaText)
|
{
|
if (string.IsNullOrEmpty(formulaText))
|
throw new ArgumentNullException("formulaText");
|
|
if (IsInFormulaCache(formulaText, null, out var result))
|
{
|
return result;
|
}
|
else
|
{
|
Operation operation = BuildAbstractSyntaxTree(formulaText, new ConstantRegistry(caseSensitive));
|
return BuildFormula(formulaText, null, operation);
|
}
|
}
|
|
/// <summary>
|
/// Build a .NET func for the provided formula.
|
/// </summary>
|
/// <param name="formulaText">The formula that must be converted into a .NET func.</param>
|
/// <param name="constants">Constant values for variables defined into the formula. They variables will be replaced by the constant value at pre-compilation time.</param>
|
/// <returns>A .NET func for the provided formula.</returns>
|
public Func<IDictionary<string, double>, double> Build(string formulaText, IDictionary<string, double> constants)
|
{
|
if (string.IsNullOrEmpty(formulaText))
|
throw new ArgumentNullException("formulaText");
|
|
|
ConstantRegistry compiledConstants = new ConstantRegistry(caseSensitive);
|
if (constants != null)
|
{
|
foreach (var constant in constants)
|
{
|
compiledConstants.RegisterConstant(constant.Key, constant.Value);
|
}
|
}
|
|
if (IsInFormulaCache(formulaText, compiledConstants, out var result))
|
{
|
return result;
|
}
|
else
|
{
|
Operation operation = BuildAbstractSyntaxTree(formulaText, compiledConstants);
|
return BuildFormula(formulaText, compiledConstants, operation);
|
}
|
}
|
|
/// <summary>
|
/// Add a function to the calculation engine.
|
/// </summary>
|
/// <param name="functionName">The name of the function. This name can be used in mathematical formulas.</param>
|
/// <param name="function">The implemenation of the function.</param>
|
/// <param name="isIdempotent">Does the function provide the same result when it is executed multiple times.</param>
|
public void AddFunction(string functionName, Func<double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
/// <summary>
|
/// Add a function to the calculation engine.
|
/// </summary>
|
/// <param name="functionName">The name of the function. This name can be used in mathematical formulas.</param>
|
/// <param name="function">The implemenation of the function.</param>
|
/// <param name="isIdempotent">Does the function provide the same result when it is executed multiple times.</param>
|
public void AddFunction(string functionName, Func<double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
/// <summary>
|
/// Add a function to the calculation engine.
|
/// </summary>
|
/// <param name="functionName">The name of the function. This name can be used in mathematical formulas.</param>
|
/// <param name="function">The implemenation of the function.</param>
|
/// <param name="isIdempotent">Does the function provide the same result when it is executed multiple times.</param>
|
public void AddFunction(string functionName, Func<double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
/// <summary>
|
/// Add a function to the calculation engine.
|
/// </summary>
|
/// <param name="functionName">The name of the function. This name can be used in mathematical formulas.</param>
|
/// <param name="function">The implemenation of the function.</param>
|
/// <param name="isIdempotent">Does the function provide the same result when it is executed multiple times.</param>
|
public void AddFunction(string functionName, Func<double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
/// <summary>
|
/// Add a function to the calculation engine.
|
/// </summary>
|
/// <param name="functionName">The name of the function. This name can be used in mathematical formulas.</param>
|
/// <param name="function">The implemenation of the function.</param>
|
/// <param name="isIdempotent">Does the function provide the same result when it is executed multiple times.</param>
|
public void AddFunction(string functionName, Func<double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
/// <summary>
|
/// Add a function to the calculation engine.
|
/// </summary>
|
/// <param name="functionName">The name of the function. This name can be used in mathematical formulas.</param>
|
/// <param name="function">The implemenation of the function.</param>
|
/// <param name="isIdempotent">Does the function provide the same result when it is executed multiple times.</param>
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
/// <summary>
|
/// Add a function to the calculation engine.
|
/// </summary>
|
/// <param name="functionName">The name of the function. This name can be used in mathematical formulas.</param>
|
/// <param name="function">The implemenation of the function.</param>
|
/// <param name="isIdempotent">Does the function provide the same result when it is executed multiple times.</param>
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, Func<double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double> function, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true);
|
}
|
|
public void AddFunction(string functionName, DynamicFunc<double, double> functionDelegate, bool isIdempotent = true)
|
{
|
FunctionRegistry.RegisterFunction(functionName, functionDelegate, isIdempotent, true);
|
}
|
|
/// <summary>
|
/// Add a constant to the calculation engine.
|
/// </summary>
|
/// <param name="constantName">The name of the constant. This name can be used in mathematical formulas.</param>
|
/// <param name="value">The value of the constant.</param>
|
public void AddConstant(string constantName, double value)
|
{
|
ConstantRegistry.RegisterConstant(constantName, value);
|
}
|
|
private void RegisterDefaultFunctions()
|
{
|
FunctionRegistry.RegisterFunction("sin", (Func<double, double>)Math.Sin, true, false);
|
FunctionRegistry.RegisterFunction("cos", (Func<double, double>)Math.Cos, true, false);
|
FunctionRegistry.RegisterFunction("csc", (Func<double, double>)MathUtil.Csc, true, false);
|
FunctionRegistry.RegisterFunction("sec", (Func<double, double>)MathUtil.Sec, true, false);
|
FunctionRegistry.RegisterFunction("asin", (Func<double, double>)Math.Asin, true, false);
|
FunctionRegistry.RegisterFunction("acos", (Func<double, double>)Math.Acos, true, false);
|
FunctionRegistry.RegisterFunction("tan", (Func<double, double>)Math.Tan, true, false);
|
FunctionRegistry.RegisterFunction("cot", (Func<double, double>)MathUtil.Cot, true, false);
|
FunctionRegistry.RegisterFunction("atan", (Func<double, double>)Math.Atan, true, false);
|
FunctionRegistry.RegisterFunction("acot", (Func<double, double>)MathUtil.Acot, true, false);
|
FunctionRegistry.RegisterFunction("loge", (Func<double, double>)Math.Log, true, false);
|
FunctionRegistry.RegisterFunction("log10", (Func<double, double>)Math.Log10, true, false);
|
FunctionRegistry.RegisterFunction("logn", (Func<double, double, double>)((a, b) => Math.Log(a, b)), true, false);
|
FunctionRegistry.RegisterFunction("sqrt", (Func<double, double>)Math.Sqrt, true, false);
|
FunctionRegistry.RegisterFunction("abs", (Func<double, double>)Math.Abs, true, false);
|
FunctionRegistry.RegisterFunction("if", (Func<double, double, double, double>)((a, b, c) => (a != 0.0 ? b : c)), true, false);
|
FunctionRegistry.RegisterFunction("ifless", (Func<double, double, double, double, double>)((a, b, c, d) => (a < b ? c : d)), true, false);
|
FunctionRegistry.RegisterFunction("ifmore", (Func<double, double, double, double, double>)((a, b, c, d) => (a > b ? c : d)), true, false);
|
FunctionRegistry.RegisterFunction("ifequal", (Func<double, double, double, double, double>)((a, b, c, d) => (a == b ? c : d)), true, false);
|
FunctionRegistry.RegisterFunction("ceiling", (Func<double, double>)Math.Ceiling, true, false);
|
FunctionRegistry.RegisterFunction("floor", (Func<double, double>)Math.Floor, true, false);
|
FunctionRegistry.RegisterFunction("truncate", (Func<double, double>)Math.Truncate, true, false);
|
FunctionRegistry.RegisterFunction("round", (Func<double, double>)Math.Round, true, false);
|
|
// Dynamic based arguments Functions
|
FunctionRegistry.RegisterFunction("max", (DynamicFunc<double, double>)((a) => a.Max()), true, false);
|
FunctionRegistry.RegisterFunction("min", (DynamicFunc<double, double>)((a) => a.Min()), true, false);
|
FunctionRegistry.RegisterFunction("avg", (DynamicFunc<double, double>)((a) => a.Average()), true, false);
|
FunctionRegistry.RegisterFunction("median", (DynamicFunc<double, double>)((a) => MathExtended.Median(a)), true, false);
|
|
// Non Idempotent Functions
|
FunctionRegistry.RegisterFunction("random", (Func<double>)random.NextDouble, false, false);
|
}
|
|
private void RegisterDefaultConstants()
|
{
|
ConstantRegistry.RegisterConstant("e", Math.E, false);
|
ConstantRegistry.RegisterConstant("pi", Math.PI, false);
|
}
|
|
/// <summary>
|
/// Build the abstract syntax tree for a given formula. The formula string will
|
/// be first tokenized.
|
/// </summary>
|
/// <param name="formulaText">A string containing the mathematical formula that must be converted
|
/// into an abstract syntax tree.</param>
|
/// <returns>The abstract syntax tree of the formula.</returns>
|
private Operation BuildAbstractSyntaxTree(string formulaText, ConstantRegistry compiledConstants)
|
{
|
TokenReader tokenReader = new TokenReader(cultureInfo);
|
List<Token> tokens = tokenReader.Read(formulaText);
|
|
AstBuilder astBuilder = new AstBuilder(FunctionRegistry, caseSensitive, compiledConstants);
|
Operation operation = astBuilder.Build(tokens);
|
|
if (optimizerEnabled)
|
return optimizer.Optimize(operation, this.FunctionRegistry, this.ConstantRegistry);
|
else
|
return operation;
|
}
|
|
private Func<IDictionary<string, double>, double> BuildFormula(string formulaText, ConstantRegistry compiledConstants, Operation operation)
|
{
|
return executionFormulaCache.GetOrAdd(GenerateFormulaCacheKey(formulaText, compiledConstants), v => executor.BuildFormula(operation, this.FunctionRegistry, this.ConstantRegistry));
|
}
|
|
private bool IsInFormulaCache(string formulaText, ConstantRegistry compiledConstants, out Func<IDictionary<string, double>, double> function)
|
{
|
function = null;
|
return cacheEnabled && executionFormulaCache.TryGetValue(GenerateFormulaCacheKey(formulaText, compiledConstants), out function);
|
}
|
|
private string GenerateFormulaCacheKey(string formulaText, ConstantRegistry compiledConstants)
|
{
|
return (compiledConstants != null && compiledConstants.Any()) ? $"{formulaText}@{String.Join(",", compiledConstants?.Select(x => $"{x.ConstantName}:{x.Value}"))}" : formulaText;
|
}
|
|
/// <summary>
|
/// Verify a collection of variables to ensure that all the variable names are valid.
|
/// Users are not allowed to overwrite reserved variables or use function names as variables.
|
/// If an invalid variable is detected an exception is thrown.
|
/// </summary>
|
/// <param name="variables">The colletion of variables that must be verified.</param>
|
internal void VerifyVariableNames(IDictionary<string, double> variables)
|
{
|
foreach (string variableName in variables.Keys)
|
{
|
if(ConstantRegistry.IsConstantName(variableName) && !ConstantRegistry.GetConstantInfo(variableName).IsOverWritable)
|
throw new ArgumentException(string.Format("The name \"{0}\" is a reservered variable name that cannot be overwritten.", variableName), "variables");
|
|
if (FunctionRegistry.IsFunctionName(variableName))
|
throw new ArgumentException(string.Format("The name \"{0}\" is a function name. Parameters cannot have this name.", variableName), "variables");
|
}
|
}
|
}
|
}
|