// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using FlaxEngine;
namespace FlaxEditor.Utilities
{
///
/// The Shunting-Yard algorithm parsing utilities.
///
[HideInEditor]
public static class ShuntingYard
{
///
/// The Shunting-Yard parser exception type.
///
///
public class ParsingException : Exception
{
///
/// Initializes a new instance of the class.
///
/// The message.
public ParsingException(string msg)
: base("Parsing error: " + msg)
{
}
}
///
/// Types of possible tokens used in Shunting-Yard parser.
///
[HideInEditor]
public enum TokenType
{
///
/// The number.
///
Number,
///
/// The variable.
///
Variable,
///
/// The parenthesis.
///
Parenthesis,
///
/// The operator.
///
Operator,
///
/// The white space character.
///
WhiteSpace,
};
///
/// Token representation containing its type and value.
///
public struct Token
{
///
/// Gets the token type.
///
public TokenType Type;
///
/// Gets the token value.
///
public string Value;
///
/// Initializes a new instance of the struct.
///
/// The type.
/// The value.
public Token(TokenType type, string value)
{
Type = type;
Value = value;
}
}
///
/// Represents simple mathematical operation.
///
private class Operator
{
public string Name;
public int Precedence;
public bool RightAssociative;
}
///
/// Describe all operators available for parsing.
///
private static readonly IDictionary Operators = new Dictionary
{
["+"] = new Operator { Name = "+", Precedence = 1 },
["-"] = new Operator { Name = "-", Precedence = 1 },
["*"] = new Operator { Name = "*", Precedence = 2 },
["/"] = new Operator { Name = "/", Precedence = 2 },
["^"] = new Operator { Name = "^", Precedence = 3, RightAssociative = true },
};
///
/// Describe all predefined variables for parsing (all lowercase).
///
private static readonly IDictionary Variables = new Dictionary
{
["pi"] = Math.PI,
["e"] = Math.E,
["infinity"] = double.MaxValue,
["-infinity"] = -double.MaxValue,
};
///
/// Compare operators based on precedence: ^ >> * / >> + -
///
/// The first operator.
/// The second operator.
/// The comparison result.
private static bool CompareOperators(string oper1, string oper2)
{
var op1 = Operators[oper1];
var op2 = Operators[oper2];
return op1.RightAssociative ? op1.Precedence < op2.Precedence : op1.Precedence <= op2.Precedence;
}
///
/// Assign a single character to its TokenType.
///
/// The input character.
/// The token type.
private static TokenType DetermineType(char c)
{
if (char.IsDigit(c) || c == '.')
return TokenType.Number;
if (char.IsWhiteSpace(c))
return TokenType.WhiteSpace;
if (c == '(' || c == ')')
return TokenType.Parenthesis;
var str = char.ToString(c);
if (Operators.ContainsKey(str))
return TokenType.Operator;
if (char.IsLetter(c))
return TokenType.Variable;
throw new ParsingException("wrong character");
}
///
/// First parsing step - tokenization of a string.
///
/// The input text.
/// The collection of parsed tokens.
public static IEnumerable Tokenize(string text)
{
// Prepare text
text = text.Replace(',', '.');
// Necessary to correctly parse negative numbers
var previous = TokenType.WhiteSpace;
var token = new StringBuilder();
for (int i = 0; i < text.Length; i++)
{
var type = DetermineType(text[i]);
if (type == TokenType.WhiteSpace)
continue;
token.Clear();
token.Append(text[i]);
// Handle fractions and negative numbers (dot . is considered a figure)
if (type == TokenType.Number || text[i] == '-' && previous != TokenType.Number)
{
// Parse the whole number till the end
if (i + 1 < text.Length)
{
switch (text[i + 1])
{
case 'x':
case 'X':
{
// Hexadecimal value
i++;
token.Clear();
if (i + 1 == text.Length || !StringUtils.IsHexDigit(text[i + 1]))
throw new ParsingException("invalid hexadecimal number");
while (i + 1 < text.Length && StringUtils.IsHexDigit(text[i + 1]))
{
token.Append(text[++i]);
}
var value = ulong.Parse(token.ToString(), NumberStyles.HexNumber);
token.Clear();
token.Append(value.ToString());
break;
}
default:
{
// Decimal value
while (i + 1 < text.Length && DetermineType(text[i + 1]) == TokenType.Number)
{
token.Append(text[++i]);
}
// Exponential notation
if (i + 2 < text.Length && (text[i + 1] == 'e' || text[i + 1] == 'E'))
{
token.Append(text[++i]);
if (text[i + 1] == '-' || text[i + 1] == '+')
token.Append(text[++i]);
while (i + 1 < text.Length && DetermineType(text[i + 1]) == TokenType.Number)
{
token.Append(text[++i]);
}
}
break;
}
}
}
// Discard solo '-'
if (token.Length != 1)
type = TokenType.Number;
}
else if (type == TokenType.Variable)
{
// Continue till the end of the variable
while (i + 1 < text.Length && DetermineType(text[i + 1]) == TokenType.Variable)
{
i++;
token.Append(text[i]);
}
}
previous = type;
yield return new Token(type, token.ToString());
}
}
///
/// Second parsing step - order tokens in reverse polish notation.
///
/// The input tokens collection.
/// The collection of the tokens in reverse polish notation order.
public static IEnumerable OrderTokens(IEnumerable tokens)
{
var stack = new Stack();
foreach (var tok in tokens)
{
switch (tok.Type)
{
// Number and variable tokens go directly to output
case TokenType.Variable:
case TokenType.Number:
yield return tok;
break;
// Operators go on stack, unless last operator on stack has higher precedence
case TokenType.Operator:
while (stack.Any() && stack.Peek().Type == TokenType.Operator && CompareOperators(tok.Value, stack.Peek().Value))
{
yield return stack.Pop();
}
stack.Push(tok);
break;
// Left parenthesis goes on stack
// Right parenthesis takes all symbols (...) to output
case TokenType.Parenthesis:
if (tok.Value == "(")
stack.Push(tok);
else
{
while (stack.Peek().Value != "(")
yield return stack.Pop();
stack.Pop();
}
break;
default: throw new ParsingException("wrong token");
}
}
// Pop all remaining operators from stack
while (stack.Any())
{
var tok = stack.Pop();
// Parenthesis still on stack mean a mismatch
if (tok.Type == TokenType.Parenthesis)
throw new ParsingException("mismatched parentheses");
yield return tok;
}
}
///
/// Third parsing step - evaluate reverse polish notation into single float.
///
/// The input token collection.
/// The result value.
public static double EvaluateRPN(IEnumerable tokens)
{
var stack = new Stack();
foreach (var token in tokens)
{
if (token.Type == TokenType.Number)
{
stack.Push(double.Parse(token.Value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture));
}
else if (token.Type == TokenType.Variable)
{
if (Variables.TryGetValue(token.Value.ToLower(), out double variableValue))
{
stack.Push(variableValue);
}
else
{
throw new ParsingException("unknown variable");
}
}
else
{
// In this step there always should be 2 values on stack
if (stack.Count < 2)
throw new ParsingException("evaluation error");
var f1 = stack.Pop();
var f2 = stack.Pop();
switch (token.Value)
{
case "+":
f2 += f1;
break;
case "-":
f2 -= f1;
break;
case "*":
f2 *= f1;
break;
case "/":
if (Math.Abs(f1) < 1e-7f)
throw new Exception("division by 0");
f2 /= f1;
break;
case "^":
f2 = Math.Pow(f2, f1);
break;
}
stack.Push(f2);
}
}
return stack.Pop();
}
///
/// Parses the specified text and performs the Shunting-Yard algorithm execution.
///
/// The input text.
/// The result value.
public static double Parse(string text)
{
var tokens = Tokenize(text);
var rpn = OrderTokens(tokens);
return EvaluateRPN(rpn);
}
}
}