Created new scripting system Added toggle for EventCallbackScenario for DevDebug window
438 lines
17 KiB
C#
438 lines
17 KiB
C#
using Soukoku.ExpressionParser.Parsing;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using UnityEngine.Events;
|
|
|
|
namespace Soukoku.ExpressionParser
|
|
{
|
|
/// <summary>
|
|
/// An expression evaluator.
|
|
/// </summary>
|
|
public class Evaluator
|
|
{
|
|
EvaluationContext _context;
|
|
Stack<ExpressionToken> _stack;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Evaluator"/> class.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <exception cref="System.ArgumentNullException">context</exception>
|
|
public Evaluator(EvaluationContext context)
|
|
{
|
|
_context = context ?? throw new ArgumentNullException("context");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates the specified input expression.
|
|
/// </summary>
|
|
/// <param name="input">The input expression (infix).</param>
|
|
/// <param name="coerseToBoolean">if set to <c>true</c> then the result will be coersed to boolean true/false if possible.
|
|
/// Anything not "false", "0", or "" is considered true.</param>
|
|
/// <returns></returns>
|
|
/// <exception cref="NotSupportedException">Unbalanced expression.</exception>
|
|
/// <exception cref="System.NotSupportedException"></exception>
|
|
public ExpressionToken Evaluate(string input, bool coerseToBoolean = false)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(input))
|
|
{
|
|
return coerseToBoolean ? ExpressionToken.False : new ExpressionToken(input);
|
|
}
|
|
|
|
var tokens = new InfixToPostfixTokenizer().Tokenize(input);
|
|
// resolve field value and type hints here
|
|
foreach (var token in tokens.Where(tk => tk.TokenType == ExpressionTokenType.Field))
|
|
{
|
|
token.FieldValue = _context.ResolveFieldValue(token.Value);
|
|
}
|
|
|
|
var reader = new ListReader<ExpressionToken>(tokens);
|
|
|
|
// from https://en.wikipedia.org/wiki/Reverse_Polish_notation
|
|
_stack = new Stack<ExpressionToken>();
|
|
while (!reader.IsEnd)
|
|
{
|
|
var tk = reader.Read();
|
|
switch (tk.TokenType)
|
|
{
|
|
case ExpressionTokenType.Value:
|
|
case ExpressionTokenType.DoubleQuoted:
|
|
case ExpressionTokenType.SingleQuoted:
|
|
case ExpressionTokenType.Field:
|
|
_stack.Push(tk);
|
|
break;
|
|
case ExpressionTokenType.Operator:
|
|
HandleOperator(tk.OperatorType);
|
|
break;
|
|
case ExpressionTokenType.Function:
|
|
HandleFunction(tk.Value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_stack.Count == 1)
|
|
{
|
|
var res = _stack.Pop();
|
|
if (coerseToBoolean)
|
|
{
|
|
return ConvertTokenToFalseTrue(res);
|
|
}
|
|
return res;
|
|
|
|
//if (res.IsNumeric())
|
|
//{
|
|
// return res;
|
|
//}
|
|
//else if (IsTrue(res.Value))
|
|
//{
|
|
// return ExpressionToken.True;
|
|
//}
|
|
}
|
|
throw new NotSupportedException("Unbalanced expression.");
|
|
}
|
|
|
|
public ExpressionToken ConvertTokenToFalseTrue(ExpressionToken token){
|
|
// changed form Value to ToString() so fields by themselves evalute properly
|
|
if (IsFalse(token.ToString()))
|
|
{
|
|
return ExpressionToken.False;
|
|
}
|
|
return ExpressionToken.True;
|
|
}
|
|
|
|
private void HandleFunction(string functionName)
|
|
{
|
|
var fun = _context.GetFunction(functionName);
|
|
var args = new Stack<ExpressionToken>(fun.ArgumentCount);
|
|
|
|
while (args.Count < fun.ArgumentCount)
|
|
{
|
|
args.Push(_stack.Pop());
|
|
}
|
|
|
|
_stack.Push(fun.Evaluate(_context, args.ToArray()));
|
|
}
|
|
|
|
#region operator handling
|
|
|
|
static bool IsDate(string lhs, string rhs, out DateTime lhsDate, out DateTime rhsDate)
|
|
{
|
|
lhsDate = default(DateTime);
|
|
rhsDate = default(DateTime);
|
|
|
|
if (DateTime.TryParse(lhs, out lhsDate))
|
|
{
|
|
DateTime.TryParse(rhs, out rhsDate);
|
|
return true;
|
|
}
|
|
else if (DateTime.TryParse(rhs, out rhsDate))
|
|
{
|
|
DateTime.TryParse(lhs, out lhsDate);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
bool IsNumber(string lhs, string rhs, out decimal lhsNumber, out decimal rhsNumber)
|
|
{
|
|
lhsNumber = 0;
|
|
rhsNumber = 0;
|
|
|
|
var islNum = decimal.TryParse(lhs, ExpressionToken.NumberParseStyle, _context.FormatCulture, out lhsNumber);
|
|
var isrNum = decimal.TryParse(rhs, ExpressionToken.NumberParseStyle, _context.FormatCulture, out rhsNumber);
|
|
|
|
return islNum && isrNum;
|
|
}
|
|
static bool IsBoolean(string lhs, string rhs, out bool lhsBool, out bool rhsBool)
|
|
{
|
|
bool lIsBool = false;
|
|
bool rIsBool = false;
|
|
lhsBool = false;
|
|
rhsBool = false;
|
|
|
|
if (!string.IsNullOrEmpty(lhs))
|
|
{
|
|
if (string.Equals(lhs, "true", StringComparison.OrdinalIgnoreCase) || lhs == "1")
|
|
{
|
|
lhsBool = true;
|
|
lIsBool = true;
|
|
}
|
|
else if (string.Equals(lhs, "false", StringComparison.OrdinalIgnoreCase) || lhs == "0")
|
|
{
|
|
lIsBool = true;
|
|
}
|
|
}
|
|
|
|
if (lIsBool && !string.IsNullOrEmpty(rhs))
|
|
{
|
|
if (string.Equals(rhs, "true", StringComparison.OrdinalIgnoreCase) || rhs == "1")
|
|
{
|
|
rhsBool = true;
|
|
rIsBool = true;
|
|
}
|
|
else if (string.Equals(rhs, "false", StringComparison.OrdinalIgnoreCase) || rhs == "0")
|
|
{
|
|
rIsBool = true;
|
|
}
|
|
}
|
|
return lIsBool && rIsBool;
|
|
|
|
//lhsBool = false;
|
|
//rhsBool = false;
|
|
|
|
//if (string.Equals(lhs, "true", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(rhs))
|
|
//{
|
|
// lhsBool = true;
|
|
// rhsBool = IsTrue(rhs);
|
|
// return true;
|
|
//}
|
|
//else if (string.Equals(lhs, "false", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(rhs))
|
|
//{
|
|
// rhsBool = IsTrue(rhs);
|
|
// return true;
|
|
//}
|
|
//else if (string.Equals(rhs, "true", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(lhs))
|
|
//{
|
|
// rhsBool = true;
|
|
// lhsBool = IsTrue(lhs);
|
|
// return true;
|
|
//}
|
|
//else if (string.Equals(rhs, "false", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(lhs))
|
|
//{
|
|
// lhsBool = IsTrue(lhs);
|
|
// return true;
|
|
//}
|
|
//return false;
|
|
}
|
|
|
|
private void HandleOperator(OperatorType op)
|
|
{
|
|
switch (op)
|
|
{
|
|
case OperatorType.Addition:
|
|
BinaryNumberOperation((a, b) => a + b);
|
|
break;
|
|
case OperatorType.Subtraction:
|
|
BinaryNumberOperation((a, b) => a - b);
|
|
break;
|
|
case OperatorType.Multiplication:
|
|
BinaryNumberOperation((a, b) => a * b);
|
|
break;
|
|
case OperatorType.Division:
|
|
BinaryNumberOperation((a, b) => a / b);
|
|
break;
|
|
case OperatorType.Modulus:
|
|
BinaryNumberOperation((a, b) => a % b);
|
|
break;
|
|
// these logical comparision can be date/num/string!
|
|
case OperatorType.LessThan:
|
|
var rhsToken = _stack.Pop();
|
|
var lhsToken = _stack.Pop();
|
|
var rhs = rhsToken.ToString();
|
|
var lhs = lhsToken.ToString();
|
|
|
|
if (IsNumber(lhs, rhs, out decimal lhsNum, out decimal rhsNum))
|
|
{
|
|
_stack.Push(lhsNum < rhsNum ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate))
|
|
{
|
|
_stack.Push(lhsDate < rhsDate ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else
|
|
{
|
|
_stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) < 0 ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
break;
|
|
case OperatorType.LessThanOrEqual:
|
|
rhsToken = _stack.Pop();
|
|
lhsToken = _stack.Pop();
|
|
rhs = rhsToken.ToString();
|
|
lhs = lhsToken.ToString();
|
|
|
|
if (IsNumber(lhs, rhs, out lhsNum, out rhsNum))
|
|
{
|
|
_stack.Push(lhsNum <= rhsNum ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate))
|
|
{
|
|
_stack.Push(lhsDate <= rhsDate ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else
|
|
{
|
|
_stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) <= 0 ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
break;
|
|
case OperatorType.GreaterThan:
|
|
rhsToken = _stack.Pop();
|
|
lhsToken = _stack.Pop();
|
|
rhs = rhsToken.ToString();
|
|
lhs = lhsToken.ToString();
|
|
|
|
if (IsNumber(lhs, rhs, out lhsNum, out rhsNum))
|
|
{
|
|
_stack.Push(lhsNum > rhsNum ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate))
|
|
{
|
|
_stack.Push(lhsDate > rhsDate ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else
|
|
{
|
|
_stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) > 0 ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
break;
|
|
case OperatorType.GreaterThanOrEqual:
|
|
rhsToken = _stack.Pop();
|
|
lhsToken = _stack.Pop();
|
|
rhs = rhsToken.ToString();
|
|
lhs = lhsToken.ToString();
|
|
|
|
if (IsNumber(lhs, rhs, out lhsNum, out rhsNum))
|
|
{
|
|
_stack.Push(lhsNum >= rhsNum ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate))
|
|
{
|
|
_stack.Push(lhsDate >= rhsDate ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else
|
|
{
|
|
_stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) >= 0 ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
break;
|
|
case OperatorType.Equal:
|
|
rhsToken = _stack.Pop();
|
|
lhsToken = _stack.Pop();
|
|
rhs = rhsToken.ToString();
|
|
lhs = lhsToken.ToString();
|
|
|
|
if (IsBoolean(lhs, rhs, out bool lhsBool, out bool rhsBool))
|
|
{
|
|
_stack.Push(lhsBool == rhsBool ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else if ((AllowAutoFormat(lhsToken) || AllowAutoFormat(rhsToken)) &&
|
|
IsNumber(lhs, rhs, out lhsNum, out rhsNum))
|
|
{
|
|
_stack.Push(lhsNum == rhsNum ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate))
|
|
{
|
|
_stack.Push(lhsDate == rhsDate ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else
|
|
{
|
|
_stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) == 0 ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
break;
|
|
case OperatorType.NotEqual:
|
|
rhsToken = _stack.Pop();
|
|
lhsToken = _stack.Pop();
|
|
rhs = rhsToken.ToString();
|
|
lhs = lhsToken.ToString();
|
|
|
|
if (IsBoolean(lhs, rhs, out lhsBool, out rhsBool))
|
|
{
|
|
_stack.Push(lhsBool != rhsBool ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else if ((AllowAutoFormat(lhsToken) || AllowAutoFormat(rhsToken)) &&
|
|
IsNumber(lhs, rhs, out lhsNum, out rhsNum))
|
|
{
|
|
_stack.Push(lhsNum != rhsNum ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate))
|
|
{
|
|
_stack.Push(lhsDate != rhsDate ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
else
|
|
{
|
|
_stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) != 0 ? ExpressionToken.True : ExpressionToken.False);
|
|
}
|
|
break;
|
|
case OperatorType.BitwiseAnd:
|
|
BinaryNumberOperation((a, b) => (int)a & (int)b);
|
|
break;
|
|
case OperatorType.BitwiseOr:
|
|
BinaryNumberOperation((a, b) => (int)a | (int)b);
|
|
break;
|
|
case OperatorType.LogicalAnd:
|
|
BinaryLogicOperation((a, b) => IsTrue(a) && IsTrue(b));
|
|
break;
|
|
case OperatorType.LogicalOr:
|
|
BinaryLogicOperation((a, b) => IsTrue(a) || IsTrue(b));
|
|
break;
|
|
case OperatorType.UnaryMinus:
|
|
UnaryNumberOperation(a => -1 * a);
|
|
break;
|
|
case OperatorType.UnaryPlus:
|
|
// no action
|
|
break;
|
|
case OperatorType.LogicalNegation:
|
|
UnaryLogicOperation(a => !IsTrue(a));
|
|
break;
|
|
case OperatorType.PreIncrement:
|
|
UnaryNumberOperation(a => a + 1);
|
|
break;
|
|
case OperatorType.PreDecrement:
|
|
UnaryNumberOperation(a => a - 1);
|
|
break;
|
|
// TODO: handle assignments & post increments
|
|
default:
|
|
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "The {0} operation is not currently supported.", op));
|
|
}
|
|
}
|
|
|
|
static bool AllowAutoFormat(ExpressionToken token)
|
|
{
|
|
return token.TokenType != ExpressionTokenType.Field || token.FieldValue.TypeHint != ValueTypeHint.Text;
|
|
}
|
|
|
|
static bool IsTrue(string value)
|
|
{
|
|
return string.Equals("true", value, StringComparison.OrdinalIgnoreCase) || value == "1";
|
|
}
|
|
|
|
static bool IsFalse(string value)
|
|
{
|
|
return string.Equals("false", value, StringComparison.OrdinalIgnoreCase) || value == "0" || string.IsNullOrWhiteSpace(value);
|
|
}
|
|
|
|
void UnaryNumberOperation(Func<decimal, decimal> operation)
|
|
{
|
|
var op1 = _stack.Pop().ToDecimal(_context);
|
|
var res = operation(op1);
|
|
|
|
_stack.Push(new ExpressionToken(res.ToString(_context.FormatCulture)));
|
|
}
|
|
void UnaryLogicOperation(Func<string, bool> operation)
|
|
{
|
|
var op1 = _stack.Pop();
|
|
var res = operation(op1.ToString()) ? "1" : "0";
|
|
|
|
_stack.Push(new ExpressionToken(res));
|
|
}
|
|
void BinaryLogicOperation(Func<string, string, bool> operation)
|
|
{
|
|
var op2 = _stack.Pop();
|
|
var op1 = _stack.Pop();
|
|
|
|
var res = operation(op1.ToString(), op2.ToString()) ? "1" : "0";
|
|
|
|
_stack.Push(new ExpressionToken(res));
|
|
}
|
|
void BinaryNumberOperation(Func<decimal, decimal, decimal> operation)
|
|
{
|
|
var op2 = _stack.Pop().ToDecimal(_context);
|
|
var op1 = _stack.Pop().ToDecimal(_context);
|
|
|
|
var res = operation(op1, op2);
|
|
|
|
_stack.Push(new ExpressionToken(res.ToString(_context.FormatCulture)));
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|