// Copyright (c) 2012-2024 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, ["m"] = Units.Meters2Units, ["cm"] = Units.Meters2Units / 100, ["km"] = Units.Meters2Units * 1000, ["s"] = 1, ["ms"] = 0.001, ["min"] = 60, ["h"] = 3600, ["cm²"] = (Units.Meters2Units / 100) * (Units.Meters2Units / 100), ["cm³"] = (Units.Meters2Units / 100) * (Units.Meters2Units / 100) * (Units.Meters2Units / 100), ["dm²"] = (Units.Meters2Units / 10) * (Units.Meters2Units / 10), ["dm³"] = (Units.Meters2Units / 10) * (Units.Meters2Units / 10) * (Units.Meters2Units / 10), ["l"] = (Units.Meters2Units / 10) * (Units.Meters2Units / 10) * (Units.Meters2Units / 10), ["m²"] = Units.Meters2Units * Units.Meters2Units, ["m³"] = Units.Meters2Units * Units.Meters2Units * Units.Meters2Units, ["kg"] = 1, ["g"] = 0.001, ["n"] = Units.Meters2Units }; /// /// List known units which cannot be handled as a variable easily because they contain operator symbols (mostly a forward slash). The value is the factor to calculate game units. /// private static readonly IDictionary UnitSymbols = new Dictionary { ["cm/s"] = Units.Meters2Units / 100, ["cm/s²"] = Units.Meters2Units / 100, ["m/s"] = Units.Meters2Units, ["m/s²"] = Units.Meters2Units, ["km/h"] = 1 / 3.6 * Units.Meters2Units, // Nm is here because these values are compared case-sensitive, and we don't want to confuse nanometers and Newtonmeters ["Nm"] = Units.Meters2Units * Units.Meters2Units, }; /// /// 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) || c == '²' || 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(',', '.').Replace("°", ""); foreach (var kv in UnitSymbols) { int idx; do { idx = text.IndexOf(kv.Key, StringComparison.InvariantCulture); if (idx > 0) { if (DetermineType(text[idx - 1]) != TokenType.Number) throw new ParsingException($"unit found without a number: {kv.Key} at {idx} in {text}"); if (Mathf.Abs(kv.Value - 1) < Mathf.Epsilon) text = text.Remove(idx, kv.Key.Length); else text = text.Replace(kv.Key, "*" + kv.Value); } } while (idx > 0); } // 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) { if (previous == TokenType.Number) { previous = TokenType.Operator; yield return new Token(TokenType.Operator, "*"); } // 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 : {token.Value}"); } } 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); } } // if stack has more than one item we're not finished with evaluating // we assume the remaining values are all factors to be multiplied if (stack.Count > 1) { var v1 = stack.Pop(); while (stack.Count > 0) v1 *= stack.Pop(); return v1; } 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); } } }