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
}
}