using System.Text;
namespace NTDLS.ExpressionParser
{
///
/// Represents a mathematical expression.
///
public class Expression
{
///
/// Delegate for calling a custom function.
///
public delegate double CustomFunction(double[] parameters);
internal readonly Dictionary CustomFunctions = new();
internal readonly double[] ComputedCache;
internal readonly HashSet DiscoveredFunctions = new();
private int _nextComputedCacheIndex = 0;
private readonly Dictionary _definedParameters = new();
private readonly HashSet _discoveredVariables = new();
private readonly int _operationCount = 0;
private readonly string _text;
private readonly StringBuilder _replaceRangeBuilder = new();
private string _workingText = string.Empty;
internal (int Index, string Placeholder) ConsumeNextComputedCacheIndex()
{
var index = _nextComputedCacheIndex;
_nextComputedCacheIndex++;
return (index, $"${index}$");
}
///
/// Represents a mathematical expression.
///
public Expression(string text)
{
var sanitized = Sanitize(text);
_text = sanitized.Text;
_operationCount = sanitized.OperationCount;
ComputedCache = new double[_operationCount];
}
///
/// Evaluates a mathematical expression.
///
/// Mathematical expression in string form.
/// Output parameter for the operational explanation.
public static double Evaluate(string expression, out string showWork)
=> new Expression(expression).Evaluate(out showWork);
///
/// Evaluates a mathematical expression.
///
/// Mathematical expression in string form.
public static double Evaluate(string expression)
=> new Expression(expression).Evaluate();
///
/// Adds a parameter to the mathematical expression.
///
/// Name of the variable as found in the string mathematical expression.
/// Value of the variable.
public void AddParameter(string name, double value) => _definedParameters.Add(name, value);
///
/// Removes all parameters which have been previously added to the expression.
///
public void ClearParameters() => _definedParameters.Clear();
///
/// Adds a function to the mathematical expression.
///
/// Name of the function as found in the string mathematical expression.
/// Delegate of the function.
public void AddFunction(string name, CustomFunction function)
=> CustomFunctions.Add(name.ToLower(), function);
///
/// Removes all functions which have been previously added to the expression.
///
public void ClearFunctions() => CustomFunctions.Clear();
///
/// Evaluates the expression, processing all variables and functions.
///
public double Evaluate()
{
ResetState();
bool isComplete;
do
{
//Get a sub-expression from the whole expression.
isComplete = AcquireSubexpression(out int startIndex, out int endIndex, out var subExpression);
//Compute the sub-expression.
var resultString = subExpression.Compute();
//Replace the sub-expression in the whole expression with the result from the sub-expression computation.
_workingText = ReplaceRange(_workingText, startIndex, endIndex, resultString);
} while (!isComplete);
if (_workingText[0] == '$')
{
return ComputedCache[_workingText.ToCacheIndex()];
}
return double.Parse(_workingText);
}
///
/// Evaluates the expression, processing all variables and functions.
///
/// Output parameter for the operational explanation.
///
public double Evaluate(out string showWork)
{
ResetState();
StringBuilder work = new();
work.AppendLine("{");
bool isComplete;
do
{
//Get a sub-expression from the whole expression.
isComplete = AcquireSubexpression(out int startIndex, out int endIndex, out var subExpression);
string friendlySubExpression = SwapInCacheValues(subExpression.Text);
work.Append(" " + friendlySubExpression);
//Compute the sub-expression.
var resultString = subExpression.Compute();
work.AppendLine($" = {SwapInCacheValues(resultString)}");
//Replace the sub-expression in the whole expression with the result from the sub-expression computation.
_workingText = ReplaceRange(_workingText, startIndex, endIndex, resultString);
} while (!isComplete);
work.AppendLine($"\}\} = {SwapInCacheValues(_workingText)}");
if (_workingText[0] == '$')
{
showWork = work.ToString();
return ComputedCache[_workingText.ToCacheIndex()];
}
showWork = work.ToString();
return double.Parse(_workingText);
}
private string SwapInCacheValues(string text)
{
var copy = new string(text);
while (true)
{
int begIndex = copy.IndexOf('$');
int endIndex = copy.IndexOf('$', begIndex + 1);
if (begIndex >= 0 && endIndex > begIndex)
{
var cacheKey = copy.Substring(begIndex, (endIndex - begIndex) + 1);
copy = copy.Replace(cacheKey, ComputedCache[cacheKey.ToCacheIndex()].ToString());
}
else
{
break;
}
}
return copy;
}
internal void ResetState()
{
_nextComputedCacheIndex = 0;
_workingText = _text; //Start with a pre-sanitized/validated copy of the supplied expression text.
//Swap out all of the user supplied parameters.
foreach (var variable in _discoveredVariables)
{
if (_definedParameters.TryGetValue(variable, out var value))
{
_workingText = _workingText.Replace(variable, value.ToString());
}
else
{
throw new Exception($"Undefined variable: {variable}");
}
}
}
internal double ExpToDouble(string exp)
{
if (exp[0] == '$')
{
return ComputedCache[exp.ToCacheIndex()];
}
return double.Parse(exp);
}
internal string ReplaceRange(string original, int startIndex, int endIndex, string replacement)
{
_replaceRangeBuilder.Clear();
int i;
for (i = 0; i < startIndex; i++)
{
_replaceRangeBuilder.Append(original[i]);
}
_replaceRangeBuilder.Append(replacement);
for (i = endIndex + 1; i < original.Length; i++)
{
_replaceRangeBuilder.Append(original[i]);
}
return _replaceRangeBuilder.ToString();
}
///
/// Gets a sub-expression from WorkingText and replaces it with a token.
///
///
internal bool AcquireSubexpression(out int outStartIndex, out int outEndIndex, out SubExpression outSubExpression)
{
int lastParenIndex = _workingText.LastIndexOf('(');
string subExpression = string.Empty;
if (lastParenIndex >= 0)
{
outStartIndex = lastParenIndex;
int scope = 0;
int i = lastParenIndex;
for (; i < _workingText.Length; i++)
{
if (char.IsWhiteSpace(_workingText[i]))
{
continue;
}
else if (_workingText[i] == '(')
{
subExpression += _workingText[i];
scope++;
}
else if (_workingText[i] == ')')
{
subExpression += _workingText[i];
scope--;
if (scope == 0)
{
break;
}
}
else
{
subExpression += _workingText[i];
}
}
if (scope != 0)
{
throw new Exception($"Parenthesizes mismatch when parsing subexpression.");
}
if (subExpression.StartsWith('(') == false || subExpression.EndsWith(')') == false)
{
throw new Exception($"Sub-expression should be enclosed in parenthesizes.");
}
outEndIndex = i;
outSubExpression = new SubExpression(this, subExpression);
return false;
}
else
{
outStartIndex = 0;
outEndIndex = _workingText.Length - 1;
subExpression = _workingText;
outSubExpression = new SubExpression(this, subExpression);
return true;
}
}
internal (string Text, int OperationCount) Sanitize(string expressionText)
{
expressionText = expressionText.Trim().ToLower();
int operationCount = 0;
string sanitized = string.Empty;
int scope = 0;
for (int i = 0; i < expressionText.Length;)
{
if (char.IsWhiteSpace(expressionText[i]))
{
i++;
continue;
}
else if (expressionText[i] == ',')
{
if (scope == 0)
{
throw new Exception("Unexpected comma found in expression.");
}
sanitized += expressionText[i++];
continue;
}
else if (expressionText[i] == '(')
{
operationCount++;
scope++;
sanitized += expressionText[i++];
continue;
}
else if (expressionText[i] == ')')
{
scope--;
if (scope < 0)
{
throw new Exception($"Scope fell below zero while sanitizing input.");
}
sanitized += expressionText[i++];
continue;
}
else if (expressionText[i].IsMathCharacter())
{
operationCount++;
sanitized += expressionText[i++];
continue;
}
else if (char.IsDigit(expressionText[i]))
{
string buffer = string.Empty;
for (; i < expressionText.Length; i++)
{
if (char.IsDigit(expressionText[i]) || expressionText[i] == '.')
{
buffer += expressionText[i];
}
else
{
break;
}
}
if (!buffer.IsNumeric())
{
throw new Exception($"Value is not a number: {buffer}");
}
sanitized += buffer;
continue;
}
else if (expressionText[i].IsValidVariableCharacter())
{
//Parse the variable/function name and determine which it is. If its a function, then we want to swap out the opening and closing parenthesizes with curly braces.
string functionOrVariableName = string.Empty;
bool isFunction = false;
for (; i < expressionText.Length; i++)
{
if (char.IsWhiteSpace(expressionText[i]))
{
continue;
}
else if (expressionText[i].IsValidVariableCharacter())
{
functionOrVariableName += expressionText[i];
}
else if (expressionText[i] == '(')
{
isFunction = true;
break;
}
else
{
break;
}
}
if (isFunction)
{
sanitized += functionOrVariableName; //Append the function name to the expression.
operationCount++;
DiscoveredFunctions.Add(functionOrVariableName);
string functionExpression = string.Empty;
//If its a function, then lets find the opening and closing parenthesizes and replace them with curly braces.
int functionScope = 0;
for (; i < expressionText.Length; i++)
{
if (char.IsWhiteSpace(expressionText[i]))
{
continue;
}
else if (expressionText[i] == '(')
{
functionExpression += expressionText[i];
functionScope++;
}
else if (expressionText[i] == ')')
{
functionExpression += expressionText[i];
functionScope--;
if (functionScope == 0)
{
i++; //Consume the closing paren.
break;
}
}
else
{
functionExpression += expressionText[i];
}
}
if (functionScope != 0)
{
throw new Exception($"Parenthesizes mismatch when parsing function scope: {functionOrVariableName}");
}
var subExpression = Sanitize(functionExpression);
if (subExpression.Text.StartsWith('(') == false || subExpression.Text.EndsWith(')') == false)
{
throw new Exception($"The function scope should be enclosed in parenthesizes.");
}
operationCount += subExpression.OperationCount;
sanitized += string.Concat("{", subExpression.Text.AsSpan(1, subExpression.Text.Length - 2), "}");
}
else
{
sanitized += functionOrVariableName; //Append the function name to the expression.
operationCount++;
_discoveredVariables.Add(functionOrVariableName);
}
}
else
{
throw new Exception($"Unhandled character {expressionText[i]}");
}
}
if (scope != 0)
{
throw new Exception($"Scope mismatch while sanitizing input.");
}
return (sanitized, operationCount);
}
}
} Last modified by Admin @ 10/22/2025 3:56:30 PM
Comments
Login to leave a comment.View all comments