// 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); } } }