//******************************************************************************************************
// BooleanExpression.cs - Gbtc
//
// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 02/12/2016 - Stephen C. Wills
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using GSF.Collections;
namespace GSF.Parsing
{
///
/// Represents a boolean expression that can be parsed and executed at runtime.
///
///
/// Binary operators have the same level of precedence and are evaluated from right to left.
///
public class BooleanExpression
{
#region [ Members ]
// Nested Types
///
/// Represents a variable that can be tweaked at runtime.
///
public class Variable
{
///
/// The identifier used to refer to the variable.
///
public readonly string Identifier;
///
/// The value of the variable.
///
public bool Value;
///
/// Creates a new instance of the class.
///
/// The identifier used to refer to the variable.
public Variable(string identifier)
{
Identifier = identifier;
}
}
// Fields
private Dictionary m_variables;
private Func m_evaluate;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
/// The expression text to be parsed as a boolean expression.
///
/// The default comparer for identifiers is .
///
public BooleanExpression(string expressionText)
: this(expressionText, StringComparer.OrdinalIgnoreCase)
{
}
///
/// Creates a new instance of the class.
///
/// The expression text to be parsed as a boolean expression.
/// Comparer used to compare identifiers.
public BooleanExpression(string expressionText, IEqualityComparer identifierComparer)
{
StringBuilder builder;
Expression expression;
m_variables = new Dictionary(StringComparer.OrdinalIgnoreCase);
builder = new StringBuilder(expressionText);
expression = ParseExpression(builder);
if (builder.Length > 0)
throw new FormatException($"Unexpected character '{builder[0]}' in expression. Expected end of expression.");
m_evaluate = Expression.Lambda>(expression).Compile();
}
#endregion
#region [ Properties ]
///
/// Gets the list of variables found while parsing the boolean expression.
///
public List Variables
{
get
{
return m_variables.Values.ToList();
}
}
///
/// Gets the variable identified by the given identifier.
///
/// The identifier used to refer to the variable.
/// The variable identified by the given identifier.
public Variable this[string identifier]
{
get
{
return m_variables[identifier];
}
}
#endregion
#region [ Methods ]
///
/// Evaluates the expression using the
/// current values of the variables.
///
/// The result of the evalulation.
public bool Evaluate()
{
return m_evaluate();
}
///
/// Attempts to get the variable identified by the given identifier.
///
/// The identifier used to refer to the variable.
/// The variable identified by the given identifier.
/// True if the variable is present in the expression; false otherwise.
public bool TryGetVariable(string identifier, out Variable variable)
{
return m_variables.TryGetValue(identifier, out variable);
}
private Expression ParseExpression(StringBuilder builder)
{
Expression subexpression;
char binaryOp;
subexpression = ParseSubexpression(builder);
ShedWhitespace(builder);
if (builder.Length > 0)
{
binaryOp = builder[0];
if (binaryOp == ')')
return subexpression;
builder.Remove(0, 1);
ShedWhitespace(builder);
switch (binaryOp)
{
case '&':
return Expression.And(subexpression, ParseExpression(builder));
case '|':
return Expression.Or(subexpression, ParseExpression(builder));
case '^':
return Expression.ExclusiveOr(subexpression, ParseExpression(builder));
default:
throw new FormatException($"Unexpected character '{binaryOp}' in expression. Expected: '&', '|', or '^'.");
}
}
return subexpression;
}
private Expression ParseSubexpression(StringBuilder builder)
{
Expression expression;
ShedWhitespace(builder);
if (builder.Length == 0)
throw new FormatException($"Unexpected end of expression. Expected: '(', '!', '~', or identifier.");
switch (builder[0])
{
case '(':
builder.Remove(0, 1);
expression = ParseExpression(builder);
if (builder.Length == 0)
throw new FormatException($"Unexpected end of expression. Expected: ')'.");
if (builder[0] != ')')
throw new FormatException($"Unexpected character '{builder[0]}' in expression. Expected: ')'.");
builder.Remove(0, 1);
return expression;
case '!':
case '~':
builder.Remove(0, 1);
return Expression.Not(ParseSubexpression(builder));
}
return ParseIdentifier(builder);
}
private Expression ParseIdentifier(StringBuilder builder)
{
Variable identifier;
StringBuilder nameBuilder = new StringBuilder();
string name;
while (builder.Length > 0 && char.IsLetterOrDigit(builder[0]))
{
nameBuilder.Append(builder[0]);
builder.Remove(0, 1);
}
if (nameBuilder.Length == 0)
throw new FormatException($"Unexpected character '{builder[0]}' in expression. Expected identifier.");
name = nameBuilder.ToString();
identifier = m_variables.GetOrAdd(name, key => new Variable(key));
return ((Expression>)(() => identifier.Value)).Body;
}
private void ShedWhitespace(StringBuilder builder)
{
while (builder.Length > 0 && char.IsWhiteSpace(builder[0]))
builder.Remove(0, 1);
}
#endregion
}
}