В первой части о TParser, Я указал ссылку на статью, где уже реализован анализатор арифметических выражений на основе TParser. Также, сказал что там есть ошибка в реализации, и связанна эта ошибка как раз с классом TParser. Последующие рассуждения будут касаться этой статьи, поэтому надо как минимум с ней ознакомится , ну а как максимум попробовать запустить код. Ничего сложного там нет.
Данный анализатор сделан по принципу левостороннего рекурсивного нисходящего анализа, принципы (и не только) которого очень хорошо освещено в данной статье.
Итак начнем. Если вы просмотрели статью о анализаторе , то там автор статьи задает вопрос
.
..простой пример, который мы хотим парсировать:
(23.34 + 21.21) * 2.92 - 12.21 * sin (180) * -1
Мои расчеты вашего выражения привели к результату 130.086 - это правильно?
| Запустив программу которую приводит автор и вставив выражение, мы действительно получим результат 130.086.
Разобьем данное выражение на две части (23.34 + 21.21) * 2.92 и 12.21 * sin (180) * -1. И теперь подсчитаем каждую часть отдельно(можно в той же программе или в обычном калькуляторе) Первая часть нам дает
(23.34 + 21.21) * 2.92 = 130.086
а Вот вторая дает следующий результат
12.21 * sin (180) * -1 = 9,78207368231007
И конечный результат оказывается должен быть таким
130,086 - 9,78207368231007 = 120.30392631769
Вот Мы и ответили на вопрос автора...
Вся загвоздка оказалась в том, что данный анализатор не умеет вычитать. Умножать, делить, складывать, находить синусы, косинусы, соблюдать приоритет операций и скобок (!) он умеет, в вычитать - нет. И причина не в коде анализатора, причина в TParser. Оказывается '+ ',' * ',' / ' TParser относит просто к одиночным символам, а вот '-' он переводит в свою лексему как .... целое число (toInteger), при чем если несколько минусов идут подряд ('--'), то как вещественное число (toFloat). Связанно это с распознаванием TParser вещественных чисел. Поэтому выражение "5 - 3" он распознает как три лексемы типа toInteger.
Для решения данной проблемы, существует несколько способов решения. Для начало воспользуемся самым простым. Будем править идеальный код анализатора.
Для этого в коде анализатора находим метод-функцию SkipToken
| Delphi | 1
2
3
4
5
6
| function TExpressionParser.SkipToken(Value: char): boolean;
begin
Result := Token = Value;
if Result then
NextToken;
end; |
|
и заменяем Token на TokenString.
В результате получаем:
| Delphi | 1
2
3
4
5
6
| function TExpressionParser.SkipToken(Value: char): boolean;
begin
Result := TokenString = Value;
if Result then
NextToken;
end; |
|
Все, теперь наш анализатор умеет вычитать. Можете проверить вышеприведенное выражение .
Но это еще не все. Как оказалось анализатор не может работать с минусом перед скобками, например
-(23.34 + 21.21) = вызов исключения
Для этого придется еще подправить две функции EvalFactor и EvalItem. Я не буду показывать их старый код (они есть в статье и можете сами сравнить), а приведу уже измененный вариант.
| Delphi | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function TExpressionParser.EvalFactor: double;
begin
if TokenString = '-' then
begin
NextToken;
Result := -EvalItem;
end
else if TokenString = '+' then
begin
NextToken;
Result := EvalItem;
end
else
Result := EvalItem;
end; |
|
| Delphi | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| function TExpressionParser.EvalItem: double;
var
Expr: double;
Fn: Integer;
begin
case Token of
toInteger:
if TokenString <> '-' then
Result := TokenInt
else
begin
Result := EvalFactor;
Exit;
end;
toFloat:
Result := TokenFloat;
'+':
begin
Result := EvalFactor;
Exit;
end;
'(':
begin
NextToken;
Result := EvalExpr;
CheckToken(')');
end;
toSymbol:
begin
if CompareText(TokenString, 'SIN') = 0 then
Fn := 1
else if CompareText(TokenString, 'COS') = 0 then
Fn := 2
else
raise EParserError.CreateFmt('Неизвестный элемент "%s"', [TokenString]);
NextToken;
CheckToken('(');
NextToken;
Expr := EvalExpr;
CheckToken(')');
case Fn of
1:
Result := SIN(Expr);
2:
Result := COS(Expr);
end;
end;
else
raise EParserError.CreateFmt('Неожидаемый символ "%s"', [Token]);
end;
NextToken;
end; |
|
Теперь выражение с минусом считает правильно
-(23.34 + 21.21) = -44.55
Что же, у нас получился практически функциональный анализатор арифметических выражений. Нужно только добавить еще необходимые операции. Тангенс, котангенс и другие тригонометрические функции вы сможете и сами сделать по принципу синуса и косинуса. Нужно только помнить, что вычисления в них идут через радианы. А вот возведение в степень, Я помогу вам реализовать.
У степени приоритет операции выше чем умножения и деления и тем более сложения и вычитания. Добавим еще один метод-функцию EvalBase в класс TExpressionParser, в секцию protected
| Delphi | 1
2
3
4
5
| type
TExpressionParser = class(TParser)
protected
function EvalBase : double; virtual;
.............................................. |
|
А в реализации метода напишем,
| Delphi | 1
2
3
4
5
| function TExpressionParser.EvalBase: double;
begin
Result := EvalFactor;
if SkipToken('^') then Result := power(Result,EvalBase);
end; |
|
при этом не забываем подключить модуль math.
Теперь осталось изменить в функции EvalTerm строчку Result := EvalFactor на Result := EvalBase
| Delphi | 1
2
3
4
5
6
7
8
9
| function TExpressionParser.EvalTerm: double;
begin
// Result := EvalFactor;
Result := EvalBase;
if SkipToken('*') then
Result := Result * EvalTerm
else if SkipToken('/') then
Result := Result / EvalTerm;
end; |
|
и наш анализатор готов возводить в степень. Степень может быть и выражением - "2^(3 + 5)".
Теперь поговорим о некоторых тонкостей использования анализатора. Во-первых все вещественные числа должны идти через точку. Поэтому перед использованием, ставим | Delphi | 1
| DecimalSeparator := '.'; |
|
Во- вторых, это разделители (пробелы). Пробелы всегда нужны перед знаками "-" и "+", если перед ними идет числовое значение.
| Code | 1
2
3
4
5
6
7
8
9
10
11
| 2+3 // НЕ правильно
2-3 // НЕ правильно
2 -3 // НЕ правильно
2 +3 // Правильно
2 + 3 // Правильно
-(-(-3))// Правильно
2 - 3 // Правильно
2 * 3 // Правильно
2*3 // Правильно
2 / 3 // Правильно
2/3 // Правильно |
|
Если после минуса (без пробела) идет число, то анализатор распознает такую лексему как отрицательное число. Поэтому "2 -3" и "2 - 3" это абсолютно разные вещи. В первом случает анализатор распознает просто два числа "2" и "-3" идущие друг за другом, во-втором случае будет вычислено выражение со значением "-1". Но если будет так "2 * -3" или "2 * - 3" или "2*-3"- будет вычислено выражение два умноженное на минус три. Во общем старайтесь оформлять выражение, использую после каждой лексемы пробел - как показано в самом первом выражении.
Ну и на по следок покажу один из вариантов вызова анализатора
| Delphi | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| Uses ExpParse;
.......
var
Stream: TStringStream;
ExpressionParser: TExpressionParser;
begin
DecimalSeparator := '.';
Stream := TStringStream.Create('(23.34 + 21.21) * 2.92 - 12.21 * sin (180) * -1');
try
ExpressionParser := TExpressionParser.Create(Stream);
try
Text := floattostr(ExpressionParser.EvalExpr);
finally
ExpressionParser.Free;
end;
finally
Stream.Free;
end; |
|
Я не стал рассматривать вариант, когда надо править класс TParser. Статья и так получилась большой. Если будут пожелания, то в дальнейшем можем рассмотреть и такой вариант....
Готовый модуль модифицированного анализатора
ExpParse.rar
|