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