using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; namespace GraphingCalculator { public class Expression { #region Types public enum TokenType { None, Literal, Identifier, Operator, ArgSeparator, LParanthesis, RParanthesis, Function }; /// /// Represents a token /// protected class Token { public TokenType Type { get; set; } public string Text { get; set; } public Token(TokenType type, string text) { Type = type; Text = text; } } #endregion #region Variables, properties private string expressionString; private bool parsed = false; protected List Tokens { get; private set; } protected List Postfix { get; private set; } public Dictionary Variables { get; private set; } public string ExpressionString { get { return expressionString; } set { expressionString = value; parsed = false; } } #endregion #region Other stuff public void AddVariable(string name, float value) { Variables.Add(name, value); } #endregion #region Constructor public Expression() { Tokens = new List(); Postfix = new List(); Variables = new Dictionary(); Variables.Add("pi", Mathf.PI); } public Expression(string expr) : this() { expressionString = expr; } #endregion #region Parser private void AnalyzeLex() { Tokens.Clear(); for (int i = 0; i < ExpressionString.Length; i++) { if (char.IsWhiteSpace(ExpressionString[i])) continue; if (IsOperator(ExpressionString[i])) { // Handle unary minus if (IsUnaryMinus(ExpressionString[i])) Tokens.Add(new Token(TokenType.Operator, "u")); else Tokens.Add(new Token(TokenType.Operator, ExpressionString[i].ToString())); } else if (ExpressionString[i] == '(') Tokens.Add(new Token(TokenType.LParanthesis, ExpressionString[i].ToString())); else if (ExpressionString[i] == ')') Tokens.Add(new Token(TokenType.RParanthesis, ExpressionString[i].ToString())); else if (ExpressionString[i] == ',') Tokens.Add(new Token(TokenType.ArgSeparator, ExpressionString[i].ToString())); else if (Char.IsDigit(ExpressionString[i])) Tokens.Add(new Token(TokenType.Literal, GetLiteral(ExpressionString, ref i))); else if (Char.IsLetter(ExpressionString[i])) Tokens.Add(new Token(TokenType.Identifier, GetIdentifier(ExpressionString, ref i))); else throw new Exception("Unrecognized character found!"); } } private void ConvertPostfix() { Stack stack = new Stack(); for (int i = 0; i < Tokens.Count; i++) { Token t = Tokens[i]; switch (t.Type) { case TokenType.Identifier: // Followed by '(' means function if (i + 1 < Tokens.Count && Tokens[i + 1].Type == TokenType.LParanthesis) stack.Push(new Token(TokenType.Function, t.Text)); // Else, variable else Postfix.Add(t); break; case TokenType.Literal: Postfix.Add(t); break; case TokenType.ArgSeparator: // We pop everything from the stack until left paranthesis open while (stack.Peek().Type != TokenType.LParanthesis) { Postfix.Add(stack.Pop()); if (stack.Count == 0) throw new Exception("Syntax error! Unexpected comma."); } break; case TokenType.Operator: if (IsLeftAssociative(t.Text)) { while (stack.Count != 0 && Precedence(t.Text) <= Precedence(stack.Peek().Text)) Postfix.Add(stack.Pop()); } else { while (stack.Count != 0 && Precedence(t.Text) < Precedence(stack.Peek().Text)) Postfix.Add(stack.Pop()); } stack.Push(t); break; case TokenType.LParanthesis: stack.Push(t); break; case TokenType.RParanthesis: while (stack.Peek().Type != TokenType.LParanthesis) { Postfix.Add(stack.Pop()); if (stack.Count == 0) throw new Exception("Mismatched parantheses!"); } stack.Pop(); // Pop Lparanthesis if (stack.Count > 0 && stack.Peek().Type == TokenType.Function) Postfix.Add(stack.Pop()); // Pop function name break; } } while (stack.Count > 0) { if (stack.Peek().Type == TokenType.LParanthesis) throw new Exception("Mismatched parantheses!"); Postfix.Add(stack.Pop()); } } public void ParseExpression() { if (!parsed) { Tokens.Clear(); Postfix.Clear(); AnalyzeLex(); ConvertPostfix(); } } public float Evaluate() { // Parse expression first ParseExpression(); // Expression is empty, so is result if (Postfix.Count == 0) return 0; Stack stack = new Stack(); foreach (var t in Postfix) { switch (t.Type) { // We already replace functions, so identifiers are variables case TokenType.Identifier: if (!Variables.ContainsKey(t.Text)) throw new Exception("Undefined variable '" + t.Text + "'."); stack.Push(Variables[t.Text]); break; case TokenType.Literal: stack.Push(float.Parse(t.Text)); break; case TokenType.Operator: switch (t.Text) { case "u": stack.Push(stack.Pop() * -1); break; case "+": stack.Push(stack.Pop() + stack.Pop()); break; case "-": { float b = stack.Pop(); float a = stack.Pop(); stack.Push(a - b); } break; case "*": stack.Push(stack.Pop() * stack.Pop()); break; case "/": { float b = stack.Pop(); float a = stack.Pop(); stack.Push(a / b); } break; case "%": { float b = stack.Pop(); float a = stack.Pop(); stack.Push(a % b); } break; case "^": { float b = stack.Pop(); float a = stack.Pop(); stack.Push(Mathf.Pow(a, b)); } break; } break; case TokenType.Function: EvaluateFunction(t.Text, ref stack); break; } } return stack.Pop(); } #endregion #region Helper routines private bool IsUnaryMinus(char c) { if (c == '-') { // Nothing in front, definitely unary if (Tokens.Count == 0) return true; // See what's in front TokenType inFront = Tokens.Last().Type; // If what's in front cannot be an operand, than it is unary minus return inFront == TokenType.ArgSeparator || inFront == TokenType.LParanthesis || inFront == TokenType.Operator; } return false; } private void EvaluateFunction(string func, ref Stack stack) { switch (func) { case "sin": stack.Push(Mathf.Sin(stack.Pop())); break; case "cos": stack.Push(Mathf.Cos(stack.Pop())); break; case "tan": stack.Push(Mathf.Tan(stack.Pop())); break; case "ctan": stack.Push(1 / Mathf.Tan(stack.Pop())); break; case "arcsin": stack.Push(Mathf.Asin(stack.Pop())); break; case "asin": stack.Push(Mathf.Asin(stack.Pop())); break; case "arccos": stack.Push(Mathf.Acos(stack.Pop())); break; case "acos": stack.Push(Mathf.Acos(stack.Pop())); break; case "arctan": stack.Push(Mathf.Atan(stack.Pop())); break; case "atan": stack.Push(Mathf.Atan(stack.Pop())); break; case "truncate": case "floor": stack.Push(Mathf.Floor(stack.Pop())); break; case "ceil": case "ceiling": stack.Push(Mathf.Ceil(stack.Pop())); break; case "sqrt": stack.Push(Mathf.Sqrt(stack.Pop())); break; case "cbrt": stack.Push(Mathf.Pow(stack.Pop(), 1.0f / 3.0f)); break; case "root": stack.Push(Mathf.Pow(stack.Pop(), 1 / stack.Pop())); break; case "abs": stack.Push(Math.Abs(stack.Pop())); break; case "max": stack.Push(Math.Max(stack.Pop(), stack.Pop())); break; case "min": stack.Push(Math.Min(stack.Pop(), stack.Pop())); break; case "lg": stack.Push(Mathf.Log10(stack.Pop())); break; case "log": stack.Push(Mathf.Log(stack.Pop(), stack.Pop())); break; case "clamp01": stack.Push(Mathf.Clamp01(stack.Pop())); break; case "clamp": stack.Push(Mathf.Clamp(stack.Pop(), stack.Pop(), stack.Pop())); break; case "lerp": stack.Push(Mathf.Lerp(stack.Pop(), stack.Pop(), stack.Pop())); break; default: throw new Exception("Undefined function '" + func + "'."); } } private static bool IsLeftAssociative(string op) { return (op != "^"); } private static int Precedence(string op) { switch (op) { case "+": case "-": return 1; case "*": case "/": case "%": return 2; case "u": case "^": return 3; default: return 0; } } private static bool IsOperator(char c) { const string operators = "+-*/%^"; return operators.Contains(c); } private static string GetIdentifier(string s, ref int index) { int start = index; while (index < s.Length && (char.IsLetterOrDigit(s[index]) || s[index] == '_')) ++index; index -= 1; return s.Substring(start, index + 1 - start); } private static string GetLiteral(string s, ref int index) { int start = index; while (index < s.Length && (char.IsDigit(s[index]) || s[index] == '.')) ++index; index -= 1; return s.Substring(start, index + 1 - start); } #endregion } }