/// <summary>
        /// Evaluates each parameter of a function's parameter list and returns
        /// a list of those values. An empty list is returned if no parameters
        /// were found. It is assumed the current position is at the opening
        /// parenthesis of the argument list.
        /// </summary>
        /// <param name="parser">ParsingHelper object.</param>
        /// <returns>A list of parameter values.</returns>
        private Variable[] ParseParameters(ParsingHelper parser)
        {
            List <Variable> parameters = new();

            // Move past open parenthesis
            parser++;
            parser.SkipWhiteSpace();
            // If function has any parameters
            if (parser.Peek() != ')')
            {
                // Parse function parameter list
                int start      = parser.Index;
                int parenCount = 1;

                while (!parser.EndOfText)
                {
                    if (parser.Peek() == ',')
                    {
                        // Note: Ignore commas inside parentheses. They could be
                        // for a parameter list in a function inside the parameters
                        if (parenCount == 1)
                        {
                            parameters.Add(ParseParameter(parser, start));
                            start = parser.Index + 1;
                        }
                    }
                    if (parser.Peek() == ')')
                    {
                        parenCount--;
                        if (parenCount == 0)
                        {
                            parameters.Add(ParseParameter(parser, start));
                            break;
                        }
                    }
                    else if (parser.Peek() == '(')
                    {
                        parenCount++;
                    }
                    parser++;
                }
            }
            // Make sure we found a closing parenthesis
            if (parser.Peek() != ')')
            {
                throw new ExpressionException(ErrClosingParenExpected, parser.Index);
            }
            // Move past closing parenthesis
#pragma warning disable IDE0059 // Unnecessary assignment of a value
            parser++;
#pragma warning restore IDE0059 // Unnecessary assignment of a value
            // Return parameter list
            return(parameters.ToArray());
        }
        /// <summary>
        /// Converts a standard infix expression to list of tokens in
        /// postfix order.
        /// </summary>
        /// <param name="expression">Expression to evaluate.</param>
        /// <returns>List of tokens in postfix order.</returns>
        private List <IToken> TokenizeExpression(string expression)
        {
            ParsingHelper  parser     = new ParsingHelper(expression);
            List <IToken>  tokens     = new List <IToken>();
            Stack <IToken> stack      = new Stack <IToken>();
            State          state      = State.None;
            int            parenCount = 0;
            IToken         token;

            while (true)
            {
                parser.SkipWhiteSpace();
                if (parser.EndOfText)
                {
                    break;
                }

                // Get next character
                char c = parser.Peek();
                if (c == '(')
                {
                    // Cannot follow operand
                    if (state == State.Operand)
                    {
                        throw new ExpressionException(ErrOperatorExpected, parser.Index);
                    }
                    // Allow additional unary operators after "("
                    if (state == State.UnaryOperator)
                    {
                        state = State.Operator;
                    }
                    // Push opening parenthesis onto stack
                    stack.Push(new LeftParenthesisToken());
                    // Track number of parentheses
                    parenCount++;
                }
                else if (c == ')')
                {
                    // Must follow operand
                    if (state != State.Operand)
                    {
                        throw new ExpressionException(ErrOperandExpected, parser.Index);
                    }
                    // Must have matching open parenthesis
                    if (parenCount == 0)
                    {
                        throw new ExpressionException(ErrUnmatchedClosingParen, parser.Index);
                    }
                    // Pop all operators until matching "(" found
                    token = stack.Pop();
                    while (token.Type != TokenType.LeftParenthesis)
                    {
                        tokens.Add(token);
                        token = stack.Pop();
                    }
                    // Track number of parentheses
                    parenCount--;
                }
                else if (OperatorToken.GetOperatorInfo(c, out OperatorInfo info))
                {
                    // Need a bit of extra code to support unary operators
                    if (state == State.Operand)
                    {
                        // Pop operators with precedence >= current operator
                        while (stack.Count > 0 && stack.Peek().Precedence >= info.Precedence)
                        {
                            tokens.Add(stack.Pop());
                        }
                        stack.Push(new OperatorToken(info));
                        state = State.Operator;
                    }
                    else if (state == State.UnaryOperator)
                    {
                        // Don't allow two unary operators together
                        throw new ExpressionException(ErrOperandExpected, parser.Index);
                    }
                    else
                    {
                        // Test for unary operator
                        if (c == '-')
                        {
                            // Push unary minus
                            stack.Push(new OperatorToken(OperatorToken.OpNegate));
                            state = State.UnaryOperator;
                        }
                        else if (c == '+')
                        {
                            // Just ignore unary plus
                            state = State.UnaryOperator;
                        }
                        else
                        {
                            throw new ExpressionException(ErrOperandExpected, parser.Index);
                        }
                    }
                }
                else if (IsNumberChar(c))
                {
                    if (state == State.Operand)
                    {
                        // Cannot follow other operand
                        throw new ExpressionException(ErrOperatorExpected, parser.Index);
                    }
                    // Parse number
                    tokens.Add(new OperandToken(ParseNumber(parser)));
                    state = State.Operand;
                    continue;
                }
                else if (IsSymbolFirstChar(c))
                {
                    // Parse symbols and functions
                    if (state == State.Operand)
                    {
                        // Symbol or function cannot follow other operand
                        throw new ExpressionException(ErrOperatorExpected, parser.Index);
                    }

                    // Save start of symbol for error reporting
                    int symbolPos = parser.Index;

                    // Parse this symbol
                    string symbol = parser.ParseWhile(IsSymbolChar);

                    // Skip whitespace
                    parser.SkipWhiteSpace();
                    // Check for parameter list
                    Variable result;
                    if (parser.Peek() == '(')
                    {
                        // Found parameter list, evaluate function
                        result = ParseFunction(parser, symbol, symbolPos);
                    }
                    else
                    {
                        // No parameter list, evaluate symbol (variable)
                        result = ParseSymbol(symbol, symbolPos);
                    }
                    tokens.Add(new OperandToken(result));
                    state = State.Operand;
                    // Don't MoveAhead again
                    continue;
                }
                else if (c == '"' || c == '\'')
                {
                    // String literal
                    string text = parser.ParseQuotedText();
                    tokens.Add(new OperandToken(new Variable(text)));
                    state = State.Operand;
                }
                else
                {
                    // Unrecognized character
                    throw new ExpressionException(ErrUnexpectedCharacter, c, parser.Index);
                }
                parser++;
            }
            // Expression cannot end with operator
            if (state == State.Operator || state == State.UnaryOperator)
            {
                throw new ExpressionException(ErrOperandExpected, parser.Index);
            }
            // Check for balanced parentheses
            if (parenCount > 0)
            {
                throw new ExpressionException(ErrClosingParenExpected, parser.Index);
            }
            // Retrieve remaining operators from stack
            while (stack.Count > 0)
            {
                tokens.Add(stack.Pop());
            }
            return(tokens);
        }