//****************************************************************************************************** // 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.Diagnostics.CodeAnalysis; 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. /// [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")] 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 readonly Dictionary m_variables; private readonly 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) { m_variables = new Dictionary(identifierComparer); StringBuilder builder = new StringBuilder(expressionText); Expression 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 => 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] => m_variables[identifier]; #endregion #region [ Methods ] /// /// Evaluates the expression using the /// current values of the variables. /// /// The result of the evalulation. public bool Evaluate() => 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) => m_variables.TryGetValue(identifier, out variable); private Expression ParseExpression(StringBuilder builder) { Expression subexpression = ParseSubexpression(builder); ShedWhitespace(builder); if (builder.Length <= 0) return subexpression; char 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 '^'."); } } private Expression ParseSubexpression(StringBuilder builder) { 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 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) { StringBuilder nameBuilder = new StringBuilder(); 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."); string name = nameBuilder.ToString(); Variable 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 } }