391 lines
13 KiB
C#
391 lines
13 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// The Shunting-Yard algorithm parsing utilities.
|
|
/// </summary>
|
|
[HideInEditor]
|
|
public static class ShuntingYard
|
|
{
|
|
/// <summary>
|
|
/// The Shunting-Yard parser exception type.
|
|
/// </summary>
|
|
/// <seealso cref="System.Exception" />
|
|
public class ParsingException : Exception
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ParsingException"/> class.
|
|
/// </summary>
|
|
/// <param name="msg">The message.</param>
|
|
public ParsingException(string msg)
|
|
: base("Parsing error: " + msg)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Types of possible tokens used in Shunting-Yard parser.
|
|
/// </summary>
|
|
[HideInEditor]
|
|
public enum TokenType
|
|
{
|
|
/// <summary>
|
|
/// The number.
|
|
/// </summary>
|
|
Number,
|
|
|
|
/// <summary>
|
|
/// The variable.
|
|
/// </summary>
|
|
Variable,
|
|
|
|
/// <summary>
|
|
/// The parenthesis.
|
|
/// </summary>
|
|
Parenthesis,
|
|
|
|
/// <summary>
|
|
/// The operator.
|
|
/// </summary>
|
|
Operator,
|
|
|
|
/// <summary>
|
|
/// The white space character.
|
|
/// </summary>
|
|
WhiteSpace,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Token representation containing its type and value.
|
|
/// </summary>
|
|
public struct Token
|
|
{
|
|
/// <summary>
|
|
/// Gets the token type.
|
|
/// </summary>
|
|
public TokenType Type;
|
|
|
|
/// <summary>
|
|
/// Gets the token value.
|
|
/// </summary>
|
|
public string Value;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Token"/> struct.
|
|
/// </summary>
|
|
/// <param name="type">The type.</param>
|
|
/// <param name="value">The value.</param>
|
|
public Token(TokenType type, string value)
|
|
{
|
|
Type = type;
|
|
Value = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents simple mathematical operation.
|
|
/// </summary>
|
|
private class Operator
|
|
{
|
|
public string Name;
|
|
public int Precedence;
|
|
public bool RightAssociative;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Describe all operators available for parsing.
|
|
/// </summary>
|
|
private static readonly IDictionary<string, Operator> Operators = new Dictionary<string, Operator>
|
|
{
|
|
["+"] = 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 },
|
|
};
|
|
|
|
/// <summary>
|
|
/// Describe all predefined variables for parsing (all lowercase).
|
|
/// </summary>
|
|
private static readonly IDictionary<string, double> Variables = new Dictionary<string, double>
|
|
{
|
|
["pi"] = Math.PI,
|
|
["e"] = Math.E,
|
|
["infinity"] = double.MaxValue,
|
|
["-infinity"] = -double.MaxValue,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Compare operators based on precedence: ^ >> * / >> + -
|
|
/// </summary>
|
|
/// <param name="oper1">The first operator.</param>
|
|
/// <param name="oper2">The second operator.</param>
|
|
/// <returns>The comparison result.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assign a single character to its TokenType.
|
|
/// </summary>
|
|
/// <param name="c">The input character.</param>
|
|
/// <returns>The token type.</returns>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// First parsing step - tokenization of a string.
|
|
/// </summary>
|
|
/// <param name="text">The input text.</param>
|
|
/// <returns>The collection of parsed tokens.</returns>
|
|
public static IEnumerable<Token> 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());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Second parsing step - order tokens in reverse polish notation.
|
|
/// </summary>
|
|
/// <param name="tokens">The input tokens collection.</param>
|
|
/// <returns>The collection of the tokens in reverse polish notation order.</returns>
|
|
public static IEnumerable<Token> OrderTokens(IEnumerable<Token> tokens)
|
|
{
|
|
var stack = new Stack<Token>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Third parsing step - evaluate reverse polish notation into single float.
|
|
/// </summary>
|
|
/// <param name="tokens">The input token collection.</param>
|
|
/// <returns>The result value.</returns>
|
|
public static double EvaluateRPN(IEnumerable<Token> tokens)
|
|
{
|
|
var stack = new Stack<double>();
|
|
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the specified text and performs the Shunting-Yard algorithm execution.
|
|
/// </summary>
|
|
/// <param name="text">The input text.</param>
|
|
/// <returns>The result value.</returns>
|
|
public static double Parse(string text)
|
|
{
|
|
var tokens = Tokenize(text);
|
|
var rpn = OrderTokens(tokens);
|
|
return EvaluateRPN(rpn);
|
|
}
|
|
}
|
|
}
|