//******************************************************************************************************// 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>'\', '<', '>', '=', '!', '{', '}', '[', ']'</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 <, >, =, 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<>B), A>B, A>=B, A<B and A<=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; // > elseLines 1–500 of 544. Load more
//****************************************************************************************************** // 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 DNF46 using ExpressionEvaluator; #endif using 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>'\', '<', '>', '=', '!', '{', '}', '[', ']'</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 <, >, =, 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<>B), A>B, A>=B, A<B and A<=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