544 lines · 27.4 KB · C#
RawDownload
//******************************************************************************************************//  TemplateExpression.cs - Gbtc////  Copyright © 2010, 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://www.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://  ----------------------------------------------------------------------------------------------------//  06/13/2014 - J. Ritchie Carroll//       Generated original version of source code.//  09/26/2015 - J. Ritchie Carroll//       Added advanced expression evaluation option, i.e., eval{expression}////****************************************************************************************************** using System;using System.CodeDom;using System.Collections;using System.Collections.Generic;using System.Diagnostics.CodeAnalysis;using System.Linq;using System.Runtime.CompilerServices;using System.Text;using System.Text.RegularExpressions;#if DNF46using ExpressionEvaluator;#endifusing ParsedExpression = System.Tuple<string, bool, string>;using ParsedEvaluation = System.Tuple<string, string>; namespace GSF.Parsing{    /// <summary>    /// Represents a template based token substitution parser that supports binary and advanced expressions.    /// </summary>    /// <remarks>    /// <para>    /// As an example, this parser can use a templated expression of the form:    /// <code>    /// {CompanyAcronym}_{DeviceAcronym}[?{SignalType.Source}=Phasor[-{SignalType.Suffix}{SignalIndex}]]:{Signal.Acronymn}    /// </code>    /// then replace the tokens with actual values and properly evaluate the expressions.    /// Example results could look like: GPA_SHELBY-PA1:IPHA and GPA_SHELBY:FREQ    /// </para>    /// <para>    /// Parser also supports more complex C# style expressions using the "eval{}" function, e.g.:    /// <code>    /// eval{'{CompanyAcronym}'.Substring(0,3)}_{DeviceAcronym}eval{'[?{SignalType.Source}=Phasor[-{SignalType.Suffix}]]'.Length}    /// </code>    /// </para>    /// </remarks>    public class TemplatedExpressionParser    {        #region [ Members ]         // Nested Types         // Operator symbol indices (see s_operatorSymbols definition)        private struct OperatorType        {            // Using struct instead of enum to avoid casts            public const int Equality = 0;            public const int Inequality = 1;            public const int Inequality2 = 2;            public const int LessThanOrEqual = 3;            public const int LessThan = 4;            public const int GreaterThanOrEqual = 5;            public const int GreaterThan = 6;            //public const int Equality2 = 7;        }         // Constants        private const string ExpressionParser = @"^[^\{0}\{1}]*(((?<Expressions>(?'Open'\{0})[^\{0}\{1}]*))+((?'Close-Open'\{1})[^\{0}\{1}]*)+)*(?(Open)(?!))$";        private const string EvaluationParser = @"eval\{0}([^\{0}]+)\{1}";          // Fields        private readonly Regex m_expressionParser;        private readonly Regex m_evaluationParser;        private readonly string[] m_escapedReservedSymbols;        private readonly string[] m_encodedReservedSymbols;        private string m_templatedExpression;         #endregion         #region [ Constructors ]         /// <summary>        /// Creates a new <see cref="TemplatedExpressionParser"/>.        /// </summary>        public TemplatedExpressionParser() : this('{', '}', '[', ']')        {        }         /// <summary>        /// Creates a new <see cref="TemplatedExpressionParser"/> with desired delimiters.        /// </summary>        /// <param name="startTokenDelimiter">Character that identifies the beginning of a token.</param>        /// <param name="endTokenDelimiter">Character that identifies the end of a token.</param>        /// <param name="startExpressionDelimiter">Character that identifies the beginning of an expression.</param>        /// <param name="endExpressionDelimiter">Character that identifies the beginning of an expression.</param>        /// <exception cref="ArgumentException">        /// All delimiters must be unique -- or -- the symbol <c>'u'</c> is reserved for encoding and cannot be used as a delimiter.        /// </exception>        public TemplatedExpressionParser(char startTokenDelimiter, char endTokenDelimiter, char startExpressionDelimiter, char endExpressionDelimiter)        {            if (startTokenDelimiter == endTokenDelimiter ||                startTokenDelimiter == startExpressionDelimiter ||                startTokenDelimiter == endExpressionDelimiter ||                endTokenDelimiter == startExpressionDelimiter ||                endTokenDelimiter == endExpressionDelimiter ||                startExpressionDelimiter == endExpressionDelimiter)                throw new ArgumentException("All delimiters must be unique");             if (startTokenDelimiter == 'u' ||                endTokenDelimiter == 'u' ||                startExpressionDelimiter == 'u' ||                endExpressionDelimiter == 'u')                throw new ArgumentException("The symbol 'u' is reserved for encoding and cannot be used as a delimiter.");             // Define a regular expression that can parse nested binary expressions            m_expressionParser = new Regex(string.Format(ExpressionParser, startExpressionDelimiter, endExpressionDelimiter), RegexOptions.Compiled | RegexOptions.CultureInvariant);             // Define a regular expression that will find each "eval[]" expression            m_evaluationParser = new Regex(string.Format(EvaluationParser, startTokenDelimiter, endTokenDelimiter), RegexOptions.Compiled);             // Reserved symbols include all expression operators and delimiters            ReservedSymbols = new[] { '\\', '<', '>', '=', '!', startTokenDelimiter, endTokenDelimiter, startExpressionDelimiter, endExpressionDelimiter };            m_escapedReservedSymbols = ReservedSymbols.Select(symbol => $"\\{symbol}").ToArray();            m_encodedReservedSymbols = ReservedSymbols.Select(symbol => symbol.RegexEncode()).ToArray();             StartTokenDelimiter = startTokenDelimiter;            EndTokenDelimiter = endTokenDelimiter;            StartExpressionDelimiter = startExpressionDelimiter;            EndExpressionDelimiter = endExpressionDelimiter;        }         #endregion         #region [ Properties ]         /// <summary>        /// Gets or sets the templated expression to use.        /// </summary>        public string TemplatedExpression        {            get => m_templatedExpression;            set => m_templatedExpression = EncodeEscapedReservedSymbols(value);        }         /// <summary>        /// Gets the reserved symbols - this includes all delimiters and expression operators.        /// </summary>        /// <remarks>        /// The default reserved symbol list is: <c>'\', '&lt;', '&gt;', '=', '!', '{', '}', '[', ']'</c>.        /// </remarks>        public char[] ReservedSymbols { get; }         /// <summary>        /// Gets the character that identifies the beginning of a token.        /// </summary>        public char StartTokenDelimiter { get; }         /// <summary>        /// Gets the character that identifies the end of a token.        /// </summary>        public char EndTokenDelimiter { get; }         /// <summary>        /// Gets the character that identifies the start of an expression.        /// </summary>        public char StartExpressionDelimiter { get; }         /// <summary>        /// Gets the character that identifies the end of an expression.        /// </summary>        public char EndExpressionDelimiter { get; }         #endregion         #region [ Methods ]         /// <summary>        /// Executes replacements using provided <paramref name="substitutions"/> dictionary on the currently defined        /// <see cref="TemplatedExpression"/> value and optionally evaluates any expressions.        /// </summary>        /// <param name="substitutions">Dictionary of substitutions. Dictionary keys are tokens to be replaced by the values.</param>        /// <param name="ignoreCase">Determines if substitutions should be case insensitive. Defaults to <c>true</c>.</param>        /// <param name="evaluateExpressions">Determines if expressions should be evaluated. Defaults to <c>true</c>.</param>        /// <param name="escapeSubstitutionValues">Determines if reserved symbols in substitution values should be automatically escaped. Defaults to <c>true</c>.</param>        /// <returns>A string that was based on <see cref="TemplatedExpression"/> with tokens replaced and expressions evaluated.</returns>        /// <remarks>        /// <para>        /// The default token start and end delimiters are { and } but can be overridden in <see cref="TemplatedExpressionParser"/> constructor.        /// All substitution tokens surrounded by <see cref="StartTokenDelimiter"/> and <see cref="EndTokenDelimiter"/> (e.g., {token})        /// will be immediately replaced with their string equivalents before further expression evaluation. As a result of the expression        /// syntax &lt;, &gt;, =, and ! are reserved expressions symbols; <see cref="StartTokenDelimiter"/>, <see cref="EndTokenDelimiter"/>,        /// <see cref="StartExpressionDelimiter"/> and <see cref="EndExpressionDelimiter"/> are reserved delimiter symbols. To embed any reserved        /// symbol into the <see cref="TemplatedExpression"/> so that it appears in the evaluated result, escape the symbol by prefixing it        /// with a backslash, e.g., \{.        /// </para>        /// <para>        /// The default expression start and end delimiters are [ and ] but can be overridden in <see cref="TemplatedExpressionParser"/> constructor.        /// Expressions are represented in the form of [?expression[result]] and can be nested, e.g. ([?expression1[?expression2[result]]]).        /// Expressions should not contain extraneous white space for proper evaluation. Only simple boolean comparison operations are allowed        /// in expressions, e.g., A=B (or A==B), A!=B (or A&lt;&gt;B), A&gt;B, A&gt;=B, A&lt;B and A&lt;=B - nothing more. Any expression that        /// fails to evaluate will be evaluated as FALSE. Note that if both left (A) and right (B) operands can be parsed as a numeric value then        /// the expression will be numerically evaluated otherwise expression will be a culture-invariant string comparison. Nested expressions        /// are evaluated as cumulative AND operators. There is no defined nesting limit.        /// </para>        /// <para>        /// Advanced expressions can be parsed using the eval function, e.g., eval{1 + 2}. Eval function expression is delimited by the currently        /// defined token delimiters, { and } by default. The evaluation engine deals with numbers and strings. The typing of numeric literals        /// follow C# standards, suffixes such as d, f and m may be used to explicitly specify the numeric type. If no numeric suffix is provided,        /// the default type of a numeric literal is assumed to be <see cref="int"/>. String literals should be specified using single quotes        /// instead of double quotes, this includes substitution parameters, e.g., <c>eval{'{DeviceAcronym}'.Length + 1}</c>. Advanced evaluation        /// expressions using the eval function are always parsed after common expressions. Eval functions cannot be nested.        /// </para>        /// <para>        /// When <paramref name="evaluateExpressions"/> is <c>true</c> and values in <paramref name="substitutions"/> intentionally contain        /// expressions to be evaluated, then <paramref name="escapeSubstitutionValues"/> should be set to <c>false</c> so that the reserved        /// symbols in the values are not automatically escaped.        /// </para>        /// </remarks>        public string Execute(IDictionary<string, string> substitutions, bool ignoreCase = true, bool evaluateExpressions = true, bool escapeSubstitutionValues = true)        {            if (string.IsNullOrEmpty(m_templatedExpression))                return "";             string result = m_templatedExpression;             // Execute substitutions            foreach (KeyValuePair<string, string> substitution in substitutions)            {                if (ignoreCase)                    result = result.ReplaceCaseInsensitive(substitution.Key, escapeSubstitutionValues ? EscapeAndEncodeReservedSymbols(substitution.Value) : EncodeEscapedReservedSymbols(substitution.Value));                else                    result = result.Replace(substitution.Key, escapeSubstitutionValues ? EscapeAndEncodeReservedSymbols(substitution.Value) : EncodeEscapedReservedSymbols(substitution.Value));            }             if (evaluateExpressions)            {                // Parse common expressions, i.e., [?expression[result]]                List<ParsedExpression> parsedExpressions = ParseExpressions(result, ignoreCase);                 // Execute common expression replacements                foreach (ParsedExpression parsedExpression in parsedExpressions)                    result = result.Replace(parsedExpression.Item1, parsedExpression.Item2 ? parsedExpression.Item3 : "");                 // Parse evaluation expressions, i.e., eval{expression}                List<ParsedEvaluation> parsedEvaluations = ParseEvaluations(result);                 // Execute evaluation expression replacements                foreach (ParsedEvaluation parsedEvaluation in parsedEvaluations)                    result = result.Replace(parsedEvaluation.Item1, parsedEvaluation.Item2);            }             // Decode any reserved symbols that were escaped in original templated expression            return DecodeReservedSymbols(result);        }         // Encodes escaped reserved symbols in a value.        [MethodImpl(MethodImplOptions.AggressiveInlining)]        private string EncodeEscapedReservedSymbols(string value)        {            if (string.IsNullOrWhiteSpace(value))                return value;             for (int i = 0; i < m_escapedReservedSymbols.Length; i++)                value = value.Replace(m_escapedReservedSymbols[i], m_encodedReservedSymbols[i]);             return value;        }         // Escapes then encodes reserved symbols in a value.        [MethodImpl(MethodImplOptions.AggressiveInlining)]        private string EscapeAndEncodeReservedSymbols(string value)        {            if (string.IsNullOrWhiteSpace(value))                return value;             // Encode any values that have already been escaped            value = EncodeEscapedReservedSymbols(value);             // Escape any remaining reserved symbols, e.g., replace '{' with '\{'            for (int i = 0; i < ReservedSymbols.Length; i++)                value = value.Replace(ReservedSymbols[i].ToString(), m_escapedReservedSymbols[i]);             return EncodeEscapedReservedSymbols(value);        }         // Decodes reserved symbols in a value.        [MethodImpl(MethodImplOptions.AggressiveInlining)]        private string DecodeReservedSymbols(string value)        {            if (string.IsNullOrWhiteSpace(value))                return value;             for (int i = 0; i < ReservedSymbols.Length; i++)                value = value.Replace(m_encodedReservedSymbols[i], ReservedSymbols[i].ToString());             return value;        }         // Parses expressions of the form "[?expression[result]]". Expressions can be nested, e.g., "[?expression1[?expression2[result]]]".        // Returns list of complete expressions (used as base replacement text), cumulative boolean expression evaluations and expression results        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]        private List<ParsedExpression> ParseExpressions(string fieldReplacedTemplatedExpression, bool ignoreCase)        {            List<ParsedExpression> parsedExpressions = new List<ParsedExpression>();             // Find all expressions using regular expression            Match match = m_expressionParser.Match(fieldReplacedTemplatedExpression);             if (match.Success)            {                Group capturedExpressions = match.Groups["Expressions"];                StringBuilder completeExpression = new StringBuilder();                List<bool> evaluations = new List<bool>();                int depth = 0, lastDepth = 0;                 foreach (Capture capture in capturedExpressions.Captures)                {                    if (capture.Value.StartsWith($"{StartExpressionDelimiter}?", StringComparison.Ordinal))                    {                        // Found binary operation expression                        depth++;                        completeExpression.Append(capture.Value);                                                // Only apply cumulative AND logic for items at continued depth                        if (depth == lastDepth && evaluations.Count > 0)                            evaluations.RemoveAt(evaluations.Count - 1);                         lastDepth = depth;                         // Parse binary expression                        CodeBinaryOperatorExpression expression = ParseBinaryOperatorExpression(capture.Value.Substring(2), out TypeCode expressionType);                         // Evaluate binary expression                        if (expression != null)                        {                            IComparer comparer;                             // Select comparer based on expression type                            switch (expressionType)                            {                                case TypeCode.Int32:                                    comparer = Comparer<int>.Default;                                    break;                                case TypeCode.Double:                                    comparer = Comparer<double>.Default;                                    break;                                case TypeCode.String:                                    comparer = ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;                                    break;                                default:                                    throw new ArgumentOutOfRangeException();                            }                             // Compare operands                            int result = comparer.Compare(((CodePrimitiveExpression)expression.Left).Value, ((CodePrimitiveExpression)expression.Right).Value);                             // Evaluate comparison result                            bool evaluation;                             switch (expression.Operator)                            {                                case CodeBinaryOperatorType.IdentityEquality:                                    evaluation = result == 0;                                    break;                                case CodeBinaryOperatorType.IdentityInequality:                                    evaluation = result != 0;                                    break;                                case CodeBinaryOperatorType.LessThan:                                    evaluation = result < 0;                                    break;                                case CodeBinaryOperatorType.LessThanOrEqual:                                    evaluation = result <= 0;                                    break;                                case CodeBinaryOperatorType.GreaterThan:                                    evaluation = result > 0;                                    break;                                case CodeBinaryOperatorType.GreaterThanOrEqual:                                    evaluation = result >= 0;                                    break;                                default:                                    evaluation = false;                                    break;                            }                             evaluations.Add(evaluation);                        }                        else                        {                            // Expression evaluation fails if there is not an expression                            evaluations.Add(false);                        }                    }                    else if (capture.Value.StartsWith(StartExpressionDelimiter.ToString(), StringComparison.Ordinal))                    {                        if (depth > 0)                        {                            depth++;                             // Found expression result                            completeExpression.Append(capture.Value);                             // Complete closed expression                            int index = capture.Index + capture.Length;                             while (index < fieldReplacedTemplatedExpression.Length && fieldReplacedTemplatedExpression[index] == EndExpressionDelimiter)                            {                                completeExpression.Append(EndExpressionDelimiter);                                index++;                                depth--;                            }                             // Add complete expression, cumulative boolean expression evaluation and expression result to parsed expression list                            parsedExpressions.Add(new ParsedExpression(completeExpression.ToString(), evaluations.All(item => item), capture.Value.Substring(1)));                             // Reset for next expression                            completeExpression.Clear();                             if (depth > 0)                                continue;                             evaluations.Clear();                            depth = 0;                        }                        else                        {                            // Unbalanced expression - exception not expected since regex should already catch this                            throw new InvalidOperationException($"Unbalanced delimiters detected in field replaced templated expression \"{fieldReplacedTemplatedExpression}\"");                        }                    }                }            }             return parsedExpressions;        }         private CodeBinaryOperatorExpression ParseBinaryOperatorExpression(string expression, out TypeCode expressionType)        {            if (!string.IsNullOrEmpty(expression))            {                string[] operands = expression.Split(s_operatorSymbols, StringSplitOptions.None);                 // Expression is only valid if there are exactly two operands                if (operands.Length == 2)                {                    CodePrimitiveExpression leftOperand, rightOperand;                    CodeBinaryOperatorType operatorType;                     if (int.TryParse(operands[0], out int left) && int.TryParse(operands[1], out int right))                    {                        // Both operands can be compared as integers                        leftOperand = new CodePrimitiveExpression(left);                        rightOperand = new CodePrimitiveExpression(right);                        expressionType = TypeCode.Int32;                    }                    else if (double.TryParse(operands[0], out double leftF) && double.TryParse(operands[1], out double rightF))                    {                        // Both operands can be compared as doubles                        leftOperand = new CodePrimitiveExpression(leftF);                        rightOperand = new CodePrimitiveExpression(rightF);                        expressionType = TypeCode.Double;                    }                    else                    {                        // Default to string comparison of operands                        leftOperand = new CodePrimitiveExpression(operands[0]);                        rightOperand = new CodePrimitiveExpression(operands[1]);                        expressionType = TypeCode.String;                    }                     // Determine operator type - order of detection is critical                         Symbol:                    if (expression.Contains(s_operatorSymbols[OperatorType.Equality]))                        operatorType = CodeBinaryOperatorType.IdentityEquality;                         // ==                    else if (expression.Contains(s_operatorSymbols[OperatorType.Inequality]))                        operatorType = CodeBinaryOperatorType.IdentityInequality;                       // !=                    else if (expression.Contains(s_operatorSymbols[OperatorType.Inequality2]))                        operatorType = CodeBinaryOperatorType.IdentityInequality;                       // <>                    else if (expression.Contains(s_operatorSymbols[OperatorType.LessThanOrEqual]))                        operatorType = CodeBinaryOperatorType.LessThanOrEqual;                          // <=                    else if (expression.Contains(s_operatorSymbols[OperatorType.LessThan]))                        operatorType = CodeBinaryOperatorType.LessThan;                                 // <                    else if (expression.Contains(s_operatorSymbols[OperatorType.GreaterThanOrEqual]))                        operatorType = CodeBinaryOperatorType.GreaterThanOrEqual;                       // >=                    else if (expression.Contains(s_operatorSymbols[OperatorType.GreaterThan]))                        operatorType = CodeBinaryOperatorType.GreaterThan;                              // >                    else

Lines 1–500 of 544.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t