Столкнувшись с аналогичной ситуацией - необходимостью обрабатывать короткие однострочные выражения - я написал парсер. Выражения были булевой логики вида
n1 = y and n2 > z
n2 != x or (n3 > y and n4 = z)
и так далее. По-английски можно сказать, что есть атомы, соединенные AND и OR, и каждый атом имеет три элемента - атрибут левой части, оператор и значение. Потому что это было так лаконично, я думаю, что анализ был легче. Набор возможных атрибутов известен и ограничен (например, имя, размер, время). Операторы различаются по атрибутам: разные атрибуты принимают разные наборы операторов. И диапазон и формат возможных значений также варьируются в зависимости от атрибута.
Для разбора я разбил строку на пробел, используя String.Split ().
Позже я понял, что до Split () мне нужно было нормализовать входную строку - вставлять пробелы до и после скобок. Я сделал это с помощью регулярного выражения. Replace ().
Результатом разбиения является массив токенов. Затем выполняется синтаксический анализ в большом цикле for с переключателем на левом значении атрибута. С каждым циклом цикла я собирался выпить группу жетонов. Если первый токен был открытым пареном, то у группы был всего один токен в длину: сам парен. Для токенов, которые были общеизвестными именами - мои значения атрибутов - парсер должен был отбросить группу из 3 токенов, по одному для имени, оператора и значения. Если в какой-то момент токенов недостаточно, парсер выдает исключение. В зависимости от потока токенов состояние синтаксического анализатора изменится. Соединение (И, ИЛИ, XOR) предназначено для помещения предыдущего атома в стек, и когда следующий атом будет завершен, я добавлю предыдущий атом и соединю эти два атома в составной атом. И так далее. Управление состоянием происходило в конце каждого цикла парсера.
Atom current;
for (int i=0; i < tokens.Length; i++)
{
switch (tokens[i].ToLower())
{
case "name":
if (tokens.Length <= i + 2)
throw new ArgumentException();
Comparison o = (Comparison) EnumUtil.Parse(typeof(Comparison), tokens[i+1]);
current = new NameAtom { Operator = o, Value = tokens[i+2] };
i+=2;
stateStack.Push(ParseState.AtomDone);
break;
case "and":
case "or":
if (tokens.Length <= i + 3)
throw new ArgumentException();
pendingConjunction = (LogicalConjunction)Enum.Parse(typeof(LogicalConjunction), tokens[i].ToUpper());
current = new CompoundAtom { Left = current, Right = null, Conjunction = pendingConjunction };
atomStack.Push(current);
break;
case "(":
state = stateStack.Peek();
if (state != ParseState.Start && state != ParseState.ConjunctionPending && state != ParseState.OpenParen)
throw new ArgumentException();
if (tokens.Length <= i + 4)
throw new ArgumentException();
stateStack.Push(ParseState.OpenParen);
break;
case ")":
state = stateStack.Pop();
if (stateStack.Peek() != ParseState.OpenParen)
throw new ArgumentException();
stateStack.Pop();
stateStack.Push(ParseState.AtomDone);
break;
// more like that...
case "":
// do nothing in the case of whitespace
break;
default:
throw new ArgumentException(tokens[i]);
}
// insert housekeeping for parse states here
}
Это немного упрощено. Но идея состоит в том, что каждое утверждение случая довольно просто. Это легко разобрать в атомарной единице выражения. Сложная часть соединяла их всех вместе соответствующим образом.
Этот трюк был выполнен в служебной части, в конце каждого цикла, используя стек состояний и стек атомов. В зависимости от состояния парсера могут происходить разные вещи. Как я уже говорил, в каждом операторе case состояние анализатора может меняться, а предыдущее состояние помещается в стек. Затем, в конце оператора switch, если государство сообщило, что я только что закончил синтаксический анализ атома, и было ожидающее соединение, я переместил только что проанализированный атом в CompoundAtom. Код выглядит так:
state = stateStack.Peek();
if (state == ParseState.AtomDone)
{
stateStack.Pop();
if (stateStack.Peek() == ParseState.ConjunctionPending)
{
while (stateStack.Peek() == ParseState.ConjunctionPending)
{
var cc = critStack.Pop() as CompoundAtom;
cc.Right = current;
current = cc; // mark the parent as current (walk up the tree)
stateStack.Pop(); // the conjunction is no longer pending
state = stateStack.Pop();
if (state != ParseState.AtomDone)
throw new ArgumentException();
}
}
else stateStack.Push(ParseState.AtomDone);
}
Еще одна магия - EnumUtil.Parse. Это позволяет мне разбирать такие вещи, как «<», в значение enum. Предположим, вы определили свои перечисления следующим образом: </p>
internal enum Operator
{
[Description(">")] GreaterThan,
[Description(">=")] GreaterThanOrEqualTo,
[Description("<")] LesserThan,
[Description("<=")] LesserThanOrEqualTo,
[Description("=")] EqualTo,
[Description("!=")] NotEqualTo
}
Обычно Enum.Parse ищет символическое имя значения enum, а <не является допустимым символическим именем. EnumUtil.Parse () ищет вещь в описании. Код выглядит так: </p>
internal sealed class EnumUtil
{
/// <summary>
/// Returns the value of the DescriptionAttribute if the specified Enum value has one.
/// If not, returns the ToString() representation of the Enum value.
/// </summary>
/// <param name="value">The Enum to get the description for</param>
/// <returns></returns>
internal static string GetDescription(System.Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes.Length > 0)
return attributes[0].Description;
else
return value.ToString();
}
/// <summary>
/// Converts the string representation of the name or numeric value of one or more enumerated constants to an equivilant enumerated object.
/// Note: Utilised the DescriptionAttribute for values that use it.
/// </summary>
/// <param name="enumType">The System.Type of the enumeration.</param>
/// <param name="value">A string containing the name or value to convert.</param>
/// <returns></returns>
internal static object Parse(Type enumType, string value)
{
return Parse(enumType, value, false);
}
/// <summary>
/// Converts the string representation of the name or numeric value of one or more enumerated constants to an equivilant enumerated object.
/// A parameter specified whether the operation is case-sensitive.
/// Note: Utilised the DescriptionAttribute for values that use it.
/// </summary>
/// <param name="enumType">The System.Type of the enumeration.</param>
/// <param name="value">A string containing the name or value to convert.</param>
/// <param name="ignoreCase">Whether the operation is case-sensitive or not.</param>
/// <returns></returns>
internal static object Parse(Type enumType, string stringValue, bool ignoreCase)
{
if (ignoreCase)
stringValue = stringValue.ToLower();
foreach (System.Enum enumVal in System.Enum.GetValues(enumType))
{
string description = GetDescription(enumVal);
if (ignoreCase)
description = description.ToLower();
if (description == stringValue)
return enumVal;
}
return System.Enum.Parse(enumType, stringValue, ignoreCase);
}
}
Я получил эту вещь EnumUtil.Parse () откуда-то еще. Может быть здесь?