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(params T[] values); /// /// 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. /// public class CalculationEngine { private readonly IExecutor executor; private readonly Optimizer optimizer; private readonly CultureInfo cultureInfo; private readonly MemoryCache, double>> executionFormulaCache; private readonly bool cacheEnabled; private readonly bool optimizerEnabled; private readonly bool caseSensitive; private readonly Random random; /// /// Creates a new instance of the class with /// default parameters. /// public CalculationEngine() : this(new JaceOptions()) { } /// /// Creates a new instance of the class. The dynamic compiler /// is used for formula execution and the optimizer and cache are enabled. /// /// /// The required for correctly reading floating poin numbers. /// public CalculationEngine(CultureInfo cultureInfo) : this(new JaceOptions() { CultureInfo = cultureInfo }) { } /// /// Creates a new instance of the class. The optimizer and /// cache are enabled. /// /// /// The required for correctly reading floating poin numbers. /// /// The execution mode that must be used for formula execution. public CalculationEngine(CultureInfo cultureInfo, ExecutionMode executionMode) : this (new JaceOptions() { CultureInfo = cultureInfo, ExecutionMode = executionMode }) { } /// /// Creates a new instance of the class. /// /// /// The required for correctly reading floating poin numbers. /// /// The execution mode that must be used for formula execution. /// Enable or disable caching of mathematical formulas. /// Enable or disable optimizing of formulas. /// Enable or disable auto lowercasing of variables. [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 }) { } /// /// Creates a new instance of the class. /// /// /// The required for correctly reading floating poin numbers. /// /// The execution mode that must be used for formula execution. /// Enable or disable caching of mathematical formulas. /// Enable or disable optimizing of formulas. /// Enable or disable converting to lower case. /// Enable or disable the default functions. /// Enable or disable the default constants. /// Configure the maximum cache size for mathematical formulas. /// Configure the cache reduction size for mathematical formulas. [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 }) { } /// /// Creates a new instance of the class. /// /// The to configure the behaviour of the engine. public CalculationEngine(JaceOptions options) { this.executionFormulaCache = new MemoryCache, 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 Functions { get { return FunctionRegistry; } } public IEnumerable Constants { get { return ConstantRegistry; } } public double Calculate(string formulaText) { return Calculate(formulaText, new Dictionary()); } public double Calculate(string formulaText, IDictionary 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); } /// /// Build a .NET func for the provided formula. /// /// The formula that must be converted into a .NET func. /// A .NET func for the provided formula. public Func, 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); } } /// /// Build a .NET func for the provided formula. /// /// The formula that must be converted into a .NET func. /// Constant values for variables defined into the formula. They variables will be replaced by the constant value at pre-compilation time. /// A .NET func for the provided formula. public Func, double> Build(string formulaText, IDictionary 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); } } /// /// Add a function to the calculation engine. /// /// The name of the function. This name can be used in mathematical formulas. /// The implemenation of the function. /// Does the function provide the same result when it is executed multiple times. public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } /// /// Add a function to the calculation engine. /// /// The name of the function. This name can be used in mathematical formulas. /// The implemenation of the function. /// Does the function provide the same result when it is executed multiple times. public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } /// /// Add a function to the calculation engine. /// /// The name of the function. This name can be used in mathematical formulas. /// The implemenation of the function. /// Does the function provide the same result when it is executed multiple times. public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } /// /// Add a function to the calculation engine. /// /// The name of the function. This name can be used in mathematical formulas. /// The implemenation of the function. /// Does the function provide the same result when it is executed multiple times. public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } /// /// Add a function to the calculation engine. /// /// The name of the function. This name can be used in mathematical formulas. /// The implemenation of the function. /// Does the function provide the same result when it is executed multiple times. public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } /// /// Add a function to the calculation engine. /// /// The name of the function. This name can be used in mathematical formulas. /// The implemenation of the function. /// Does the function provide the same result when it is executed multiple times. public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } /// /// Add a function to the calculation engine. /// /// The name of the function. This name can be used in mathematical formulas. /// The implemenation of the function. /// Does the function provide the same result when it is executed multiple times. public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, Func function, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, function, isIdempotent, true); } public void AddFunction(string functionName, DynamicFunc functionDelegate, bool isIdempotent = true) { FunctionRegistry.RegisterFunction(functionName, functionDelegate, isIdempotent, true); } /// /// Add a constant to the calculation engine. /// /// The name of the constant. This name can be used in mathematical formulas. /// The value of the constant. public void AddConstant(string constantName, double value) { ConstantRegistry.RegisterConstant(constantName, value); } private void RegisterDefaultFunctions() { FunctionRegistry.RegisterFunction("sin", (Func)Math.Sin, true, false); FunctionRegistry.RegisterFunction("cos", (Func)Math.Cos, true, false); FunctionRegistry.RegisterFunction("csc", (Func)MathUtil.Csc, true, false); FunctionRegistry.RegisterFunction("sec", (Func)MathUtil.Sec, true, false); FunctionRegistry.RegisterFunction("asin", (Func)Math.Asin, true, false); FunctionRegistry.RegisterFunction("acos", (Func)Math.Acos, true, false); FunctionRegistry.RegisterFunction("tan", (Func)Math.Tan, true, false); FunctionRegistry.RegisterFunction("cot", (Func)MathUtil.Cot, true, false); FunctionRegistry.RegisterFunction("atan", (Func)Math.Atan, true, false); FunctionRegistry.RegisterFunction("acot", (Func)MathUtil.Acot, true, false); FunctionRegistry.RegisterFunction("loge", (Func)Math.Log, true, false); FunctionRegistry.RegisterFunction("log10", (Func)Math.Log10, true, false); FunctionRegistry.RegisterFunction("logn", (Func)((a, b) => Math.Log(a, b)), true, false); FunctionRegistry.RegisterFunction("sqrt", (Func)Math.Sqrt, true, false); FunctionRegistry.RegisterFunction("abs", (Func)Math.Abs, true, false); FunctionRegistry.RegisterFunction("if", (Func)((a, b, c) => (a != 0.0 ? b : c)), true, false); FunctionRegistry.RegisterFunction("ifless", (Func)((a, b, c, d) => (a < b ? c : d)), true, false); FunctionRegistry.RegisterFunction("ifmore", (Func)((a, b, c, d) => (a > b ? c : d)), true, false); FunctionRegistry.RegisterFunction("ifequal", (Func)((a, b, c, d) => (a == b ? c : d)), true, false); FunctionRegistry.RegisterFunction("ceiling", (Func)Math.Ceiling, true, false); FunctionRegistry.RegisterFunction("floor", (Func)Math.Floor, true, false); FunctionRegistry.RegisterFunction("truncate", (Func)Math.Truncate, true, false); FunctionRegistry.RegisterFunction("round", (Func)Math.Round, true, false); // Dynamic based arguments Functions FunctionRegistry.RegisterFunction("max", (DynamicFunc)((a) => a.Max()), true, false); FunctionRegistry.RegisterFunction("min", (DynamicFunc)((a) => a.Min()), true, false); FunctionRegistry.RegisterFunction("avg", (DynamicFunc)((a) => a.Average()), true, false); FunctionRegistry.RegisterFunction("median", (DynamicFunc)((a) => MathExtended.Median(a)), true, false); // Non Idempotent Functions FunctionRegistry.RegisterFunction("random", (Func)random.NextDouble, false, false); } private void RegisterDefaultConstants() { ConstantRegistry.RegisterConstant("e", Math.E, false); ConstantRegistry.RegisterConstant("pi", Math.PI, false); } /// /// Build the abstract syntax tree for a given formula. The formula string will /// be first tokenized. /// /// A string containing the mathematical formula that must be converted /// into an abstract syntax tree. /// The abstract syntax tree of the formula. private Operation BuildAbstractSyntaxTree(string formulaText, ConstantRegistry compiledConstants) { TokenReader tokenReader = new TokenReader(cultureInfo); List 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, 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, 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; } /// /// 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. /// /// The colletion of variables that must be verified. internal void VerifyVariableNames(IDictionary 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"); } } } }