Add a boolean expression parser.
This commit is contained in:
@@ -246,6 +246,7 @@
|
|||||||
<Compile Include="Primitives\float3.cs" />
|
<Compile Include="Primitives\float3.cs" />
|
||||||
<Compile Include="InstalledMods.cs" />
|
<Compile Include="InstalledMods.cs" />
|
||||||
<Compile Include="CryptoUtil.cs" />
|
<Compile Include="CryptoUtil.cs" />
|
||||||
|
<Compile Include="Support\BooleanExpression.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="FileSystem\D2kSoundResources.cs" />
|
<Compile Include="FileSystem\D2kSoundResources.cs" />
|
||||||
|
|||||||
294
OpenRA.Game/Support/BooleanExpression.cs
Normal file
294
OpenRA.Game/Support/BooleanExpression.cs
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2016 The OpenRA Developers (see AUTHORS)
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version. For more
|
||||||
|
* information, see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenRA.Support
|
||||||
|
{
|
||||||
|
public class BooleanExpression
|
||||||
|
{
|
||||||
|
readonly HashSet<string> variables = new HashSet<string>();
|
||||||
|
public IEnumerable<string> Variables { get { return variables; } }
|
||||||
|
|
||||||
|
readonly Token[] postfix;
|
||||||
|
|
||||||
|
enum Associativity { Left, Right }
|
||||||
|
class Token
|
||||||
|
{
|
||||||
|
public readonly string Symbol;
|
||||||
|
public readonly int Index;
|
||||||
|
public readonly int Precedence;
|
||||||
|
public readonly Associativity Associativity;
|
||||||
|
|
||||||
|
public Token(string symbol, int index, Associativity associativity, int precedence)
|
||||||
|
{
|
||||||
|
Symbol = symbol;
|
||||||
|
Index = index;
|
||||||
|
Associativity = associativity;
|
||||||
|
Precedence = precedence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BinaryOperationToken : Token
|
||||||
|
{
|
||||||
|
public BinaryOperationToken(string symbol, int index, Associativity associativity = Associativity.Left, int precedence = 0)
|
||||||
|
: base(symbol, index, associativity, precedence) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnaryOperationToken : Token
|
||||||
|
{
|
||||||
|
public UnaryOperationToken(string symbol, int index, Associativity associativity = Associativity.Right, int precedence = 1)
|
||||||
|
: base(symbol, index, associativity, precedence) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenParenToken : Token { public OpenParenToken(int index) : base("(", index, Associativity.Left, -1) { } }
|
||||||
|
class CloseParenToken : Token { public CloseParenToken(int index) : base(")", index, Associativity.Left, -1) { } }
|
||||||
|
class VariableToken : Token
|
||||||
|
{
|
||||||
|
public VariableToken(int index, string symbol)
|
||||||
|
: base(symbol, index, Associativity.Left, 0) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndToken : BinaryOperationToken { public AndToken(int index) : base("&&", index) { } }
|
||||||
|
class OrToken : BinaryOperationToken { public OrToken(int index) : base("||", index) { } }
|
||||||
|
class EqualsToken : BinaryOperationToken { public EqualsToken(int index) : base("==", index) { } }
|
||||||
|
class NotEqualsToken : BinaryOperationToken { public NotEqualsToken(int index) : base("!=", index) { } }
|
||||||
|
class NotToken : UnaryOperationToken { public NotToken(int index) : base("!", index) { } }
|
||||||
|
|
||||||
|
public BooleanExpression(string expression)
|
||||||
|
{
|
||||||
|
var openParens = 0;
|
||||||
|
var closeParens = 0;
|
||||||
|
var tokens = new List<Token>();
|
||||||
|
for (var i = 0; i < expression.Length; i++)
|
||||||
|
{
|
||||||
|
switch (expression[i])
|
||||||
|
{
|
||||||
|
case '(':
|
||||||
|
{
|
||||||
|
tokens.Add(new OpenParenToken(i));
|
||||||
|
openParens++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ')':
|
||||||
|
{
|
||||||
|
tokens.Add(new CloseParenToken(i));
|
||||||
|
if (++closeParens > openParens)
|
||||||
|
throw new InvalidDataException("Unmatched closing parenthesis at index {0}".F(i));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
// Ignore whitespace
|
||||||
|
if (char.IsWhiteSpace(expression[i]))
|
||||||
|
break;
|
||||||
|
|
||||||
|
var token = ParseSymbol(expression, ref i);
|
||||||
|
tokens.Add(token);
|
||||||
|
|
||||||
|
var variable = token as VariableToken;
|
||||||
|
if (variable != null)
|
||||||
|
variables.Add(variable.Symbol);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check parsed tree
|
||||||
|
if (!tokens.Any())
|
||||||
|
throw new InvalidDataException("Empty expression");
|
||||||
|
|
||||||
|
if (closeParens != openParens)
|
||||||
|
throw new InvalidDataException("Mismatched opening and closing parentheses");
|
||||||
|
|
||||||
|
for (var i = 0; i < tokens.Count - 1; i++)
|
||||||
|
{
|
||||||
|
// Unary tokens must be followed by a variable, another unary token, or an opening parenthesis
|
||||||
|
if (tokens[i] is UnaryOperationToken && !(tokens[i + 1] is VariableToken || tokens[i + 1] is UnaryOperationToken
|
||||||
|
|| tokens[i + 1] is OpenParenToken))
|
||||||
|
throw new InvalidDataException("Unexpected token `{0}` at index {1}".F(tokens[i].Symbol, tokens[i].Index));
|
||||||
|
|
||||||
|
// Disallow empty parentheses
|
||||||
|
if (tokens[i] is OpenParenToken && tokens[i + 1] is CloseParenToken)
|
||||||
|
throw new InvalidDataException("Empty parenthesis at index {0}".F(tokens[i].Index));
|
||||||
|
|
||||||
|
// A variable must be followed by a binary operation or by a closing parenthesis
|
||||||
|
if (tokens[i] is VariableToken && !(tokens[i + 1] is BinaryOperationToken || tokens[i + 1] is CloseParenToken))
|
||||||
|
throw new InvalidDataException("Missing binary operation at index {0}".F(tokens[i + 1].Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expressions can't start with an operation
|
||||||
|
if (tokens[0] is BinaryOperationToken)
|
||||||
|
throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[0].Symbol, tokens[0].Index));
|
||||||
|
|
||||||
|
// Expressions can't end with a binary or unary operation
|
||||||
|
if (tokens[tokens.Count - 1] is BinaryOperationToken || tokens[tokens.Count - 1] is UnaryOperationToken)
|
||||||
|
throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[tokens.Count - 1].Symbol, tokens[tokens.Count - 1].Index));
|
||||||
|
|
||||||
|
// Binary operations must be preceeded by a closing paren or a variable
|
||||||
|
// Binary operations must be followed by an opening paren, a variable, or a unary operation
|
||||||
|
for (var i = 1; i < tokens.Count - 1; i++)
|
||||||
|
{
|
||||||
|
if (tokens[i] is BinaryOperationToken && (
|
||||||
|
!(tokens[i - 1] is CloseParenToken || tokens[i - 1] is VariableToken) ||
|
||||||
|
!(tokens[i + 1] is OpenParenToken || tokens[i + 1] is VariableToken || tokens[i + 1] is UnaryOperationToken)))
|
||||||
|
throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[i].Symbol, tokens[i].Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to postfix (discarding parentheses) ready for evaluation
|
||||||
|
postfix = ToPostfix(tokens).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Token ParseSymbol(string expression, ref int i)
|
||||||
|
{
|
||||||
|
var start = i;
|
||||||
|
var c = expression[start];
|
||||||
|
|
||||||
|
// Parse operators
|
||||||
|
if (c == '!')
|
||||||
|
{
|
||||||
|
if (i < expression.Length - 1 && expression[start + 1] == '=')
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
return new NotEqualsToken(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NotToken(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '=')
|
||||||
|
{
|
||||||
|
if (i < expression.Length - 1 && expression[start + 1] == '=')
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
return new EqualsToken(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidDataException("Unexpected character '=' at index {0}".F(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '&')
|
||||||
|
{
|
||||||
|
if (i < expression.Length - 1 && expression[start + 1] == '&')
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
return new AndToken(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidDataException("Unexpected character '&' at index {0}".F(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '|')
|
||||||
|
{
|
||||||
|
if (i < expression.Length - 1 && expression[start + 1] == '|')
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
return new OrToken(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidDataException("Unexpected character '|' at index {0}".F(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan forwards until we find an invalid name character
|
||||||
|
for (; i < expression.Length; i++)
|
||||||
|
{
|
||||||
|
c = expression[i];
|
||||||
|
if (char.IsWhiteSpace(c) || c == '(' || c == ')' || c == '!' || c == '&' || c == '|' || c == '=')
|
||||||
|
{
|
||||||
|
// Put the bad character back for the next parse attempt
|
||||||
|
i--;
|
||||||
|
return new VariableToken(start, expression.Substring(start, i - start + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the rest of the string
|
||||||
|
return new VariableToken(start, expression.Substring(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ParseSymbol(VariableToken t, Dictionary<string, bool> symbols)
|
||||||
|
{
|
||||||
|
bool value;
|
||||||
|
symbols.TryGetValue(t.Symbol, out value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ApplyBinaryOperation(Stack<bool> s, Func<bool, bool, bool> f)
|
||||||
|
{
|
||||||
|
var x = s.Pop();
|
||||||
|
var y = s.Pop();
|
||||||
|
s.Push(f(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ApplyUnaryOperation(Stack<bool> s, Func<bool, bool> f)
|
||||||
|
{
|
||||||
|
var x = s.Pop();
|
||||||
|
s.Push(f(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<Token> ToPostfix(IEnumerable<Token> tokens)
|
||||||
|
{
|
||||||
|
var s = new Stack<Token>();
|
||||||
|
foreach (var t in tokens)
|
||||||
|
{
|
||||||
|
if (t is OpenParenToken)
|
||||||
|
s.Push(t);
|
||||||
|
else if (t is CloseParenToken)
|
||||||
|
{
|
||||||
|
Token temp;
|
||||||
|
while (!((temp = s.Pop()) is OpenParenToken))
|
||||||
|
yield return temp;
|
||||||
|
}
|
||||||
|
else if (t is VariableToken)
|
||||||
|
yield return t;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (s.Count > 0 && ((t.Associativity == Associativity.Right && t.Precedence < s.Peek().Precedence)
|
||||||
|
|| (t.Associativity == Associativity.Left && t.Precedence <= s.Peek().Precedence)))
|
||||||
|
yield return s.Pop();
|
||||||
|
|
||||||
|
s.Push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (s.Count > 0)
|
||||||
|
yield return s.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Evaluate(Dictionary<string, bool> symbols)
|
||||||
|
{
|
||||||
|
var s = new Stack<bool>();
|
||||||
|
foreach (var t in postfix)
|
||||||
|
{
|
||||||
|
if (t is AndToken)
|
||||||
|
ApplyBinaryOperation(s, (x, y) => y & x);
|
||||||
|
else if (t is NotEqualsToken)
|
||||||
|
ApplyBinaryOperation(s, (x, y) => y ^ x);
|
||||||
|
else if (t is OrToken)
|
||||||
|
ApplyBinaryOperation(s, (x, y) => y | x);
|
||||||
|
else if (t is EqualsToken)
|
||||||
|
ApplyBinaryOperation(s, (x, y) => y == x);
|
||||||
|
else if (t is NotToken)
|
||||||
|
ApplyUnaryOperation(s, x => !x);
|
||||||
|
else if (t is VariableToken)
|
||||||
|
s.Push(ParseSymbol((VariableToken)t, symbols));
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
OpenRA.Test/OpenRA.Game/BooleanExpressionTest.cs
Normal file
150
OpenRA.Test/OpenRA.Game/BooleanExpressionTest.cs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2016 The OpenRA Developers (see AUTHORS)
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version. For more
|
||||||
|
* information, see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using OpenRA.Support;
|
||||||
|
|
||||||
|
namespace OpenRA.Test
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class BooleanExpressionTest
|
||||||
|
{
|
||||||
|
Dictionary<string, bool> testValues = new Dictionary<string, bool>()
|
||||||
|
{
|
||||||
|
{ "true", true },
|
||||||
|
{ "false", false }
|
||||||
|
};
|
||||||
|
|
||||||
|
void AssertFalse(string expression)
|
||||||
|
{
|
||||||
|
Assert.False(new BooleanExpression(expression).Evaluate(testValues), expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssertTrue(string expression)
|
||||||
|
{
|
||||||
|
Assert.True(new BooleanExpression(expression).Evaluate(testValues), expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssertParseFailure(string expression)
|
||||||
|
{
|
||||||
|
Assert.Throws(typeof(InvalidDataException), () => new BooleanExpression(expression).Evaluate(testValues), expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "AND operation")]
|
||||||
|
public void TestAnd()
|
||||||
|
{
|
||||||
|
AssertTrue("true && true");
|
||||||
|
AssertFalse("false && false");
|
||||||
|
AssertFalse("true && false");
|
||||||
|
AssertFalse("false && true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "OR operation")]
|
||||||
|
public void TestOR()
|
||||||
|
{
|
||||||
|
AssertTrue("true || true");
|
||||||
|
AssertFalse("false || false");
|
||||||
|
AssertTrue("true || false");
|
||||||
|
AssertTrue("false || true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "Equals operation")]
|
||||||
|
public void TestEquals()
|
||||||
|
{
|
||||||
|
AssertTrue("true == true");
|
||||||
|
AssertTrue("false == false");
|
||||||
|
AssertFalse("true == false");
|
||||||
|
AssertFalse("false == true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "Not-equals (XOR) operation")]
|
||||||
|
public void TestNotEquals()
|
||||||
|
{
|
||||||
|
AssertFalse("true != true");
|
||||||
|
AssertFalse("false != false");
|
||||||
|
AssertTrue("true != false");
|
||||||
|
AssertTrue("false != true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "NOT operation")]
|
||||||
|
public void TestNOT()
|
||||||
|
{
|
||||||
|
AssertFalse("!true");
|
||||||
|
AssertTrue("!false");
|
||||||
|
AssertTrue("!!true");
|
||||||
|
AssertFalse("!!false");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "Precedence")]
|
||||||
|
public void TestPrecedence()
|
||||||
|
{
|
||||||
|
AssertTrue("true && false || true");
|
||||||
|
AssertFalse("false || false && true");
|
||||||
|
AssertTrue("true && !true || !false");
|
||||||
|
AssertFalse("false || !true && !false");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "Parenthesis")]
|
||||||
|
public void TestParens()
|
||||||
|
{
|
||||||
|
AssertTrue("(true)");
|
||||||
|
AssertTrue("((true))");
|
||||||
|
AssertFalse("(false)");
|
||||||
|
AssertFalse("((false))");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "Parenthesis and mixed operations")]
|
||||||
|
public void TestMixedParens()
|
||||||
|
{
|
||||||
|
AssertTrue("(!false)");
|
||||||
|
AssertTrue("!(false)");
|
||||||
|
AssertFalse("!(!false)");
|
||||||
|
AssertTrue("(true) || (false)");
|
||||||
|
AssertTrue("true && (false || true)");
|
||||||
|
AssertTrue("(true && false) || true");
|
||||||
|
AssertTrue("!(true && false) || false");
|
||||||
|
AssertTrue("((true != true) == false) && true");
|
||||||
|
AssertFalse("(true != false) == false && true");
|
||||||
|
AssertTrue("true || ((true != false) != !(false && true))");
|
||||||
|
AssertFalse("((true != false) != !(false && true))");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "Test parser errors")]
|
||||||
|
public void TestParseErrors()
|
||||||
|
{
|
||||||
|
AssertParseFailure("()");
|
||||||
|
AssertParseFailure("! && true");
|
||||||
|
AssertParseFailure("(true");
|
||||||
|
AssertParseFailure(")true");
|
||||||
|
AssertParseFailure("false)");
|
||||||
|
AssertParseFailure("false(");
|
||||||
|
AssertParseFailure("false!");
|
||||||
|
AssertParseFailure("true false");
|
||||||
|
AssertParseFailure("true & false");
|
||||||
|
AssertParseFailure("true | false");
|
||||||
|
AssertParseFailure("true / false");
|
||||||
|
AssertParseFailure("true & false && !");
|
||||||
|
AssertParseFailure("(true && !)");
|
||||||
|
AssertParseFailure("&& false");
|
||||||
|
AssertParseFailure("false ||");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestName = "Undefined symbols are treated as `false` values")]
|
||||||
|
public void TestUndefinedSymbols()
|
||||||
|
{
|
||||||
|
AssertFalse("undef1 || undef2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
<Compile Include="OpenRA.Mods.Common\ShapeTest.cs" />
|
<Compile Include="OpenRA.Mods.Common\ShapeTest.cs" />
|
||||||
<Compile Include="OpenRA.Game\OrderTest.cs" />
|
<Compile Include="OpenRA.Game\OrderTest.cs" />
|
||||||
<Compile Include="OpenRA.Game\PlatformTest.cs" />
|
<Compile Include="OpenRA.Game\PlatformTest.cs" />
|
||||||
|
<Compile Include="OpenRA.Game\BooleanExpressionTest.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">
|
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">
|
||||||
|
|||||||
Reference in New Issue
Block a user