Книга: О чём не пишут в книгах по Delphi
Назад: 4.5. Учет приоритета операторов
Дальше: 4.7. Полноценный калькулятор

4.6. Выражения со скобками

Порядок выполнения операций в выражении может меняться с помощью скобок. Внутри них должно находиться выражение, которое, будучи выделенным в отдельную строку, само по себе отвечает требованиям синтаксиса к выражению в целом.
Выражение, заключенное в скобки, допустимо везде, где допускается появление отдельного числа (из этого, в частности, следует, что допускаются вложенные скобки). Таким образом, мы должны расширить нашу грамматику так, чтобы аргументом операций сложения и умножения могли служить не только числа, но и выражения, заключенные в скобки. Это автоматически позволит использовать такие выражения и в качестве слагаемых, потому что слагаемое — это последовательность из одного или нескольких множителей, разделенных знаками умножения и деления. На языке БНФ все сказанное иллюстрирует листинг 4.6.
Листинг 4.6. Грамматика выражения со скобками (первое приближение)
<Expr> ::= <Term> {<Operation1> <Term>}
<Term> ::= <Factor> {<Operation2> <Factor>}
<Factor> ::= <Number> | ' (' <Expr> ')'
В этих определениях появилась рекурсия, т.к. в определении <Expr> используется (через <Term>) символ <Factor>, а в определении <Factor> — <Term>. Соответственно, подобная грамматика будет реализовываться рекурсивными функциями.
Наша грамматика не учитывает, что перед скобками может стоять знак унарной операции "+" или "-", хотя общепринятые правила записи выражений вполне допускают выражения типа 3*-(2+4). Поэтому, прежде чем приступить к созданию нового калькулятора, введем правила, допускающие такой синтаксис. Можно было бы модифицировать определение <Factor> таким образом:
<Factor> ::= <Number> | [Sign] '(' <Expr> ')'
Однако такой подход страдает отсутствием общности. В дальнейшем мы усложним наш синтаксис, введя другие типы множителей (функции, переменные). Перед каждым из них, в принципе, может стоять знак унарной операции, поэтому логичнее определить синтаксис таким образом, чтобы унарная операция допускалась вообще перед любым множителем. В этом случае можно будет слегка упростить определение <Number>, т.к. знак "+" или "-" в начале числа можно будет трактовать не как часть числа, а как унарный оператор, стоящий перед множителем, представленным в виде числовой константы.
С учетом этого новая грамматика запишется следующим образом (листинг 4.7).
Листинг 4.7. Окончательный вариант грамматики выражения со скобками
<Expr> ::= <Term> {<Operation1> <Term>}
<Term> ::= <Factor> {<Operation2> <Factor>}
<Factor> ::= <UnaryOp> <Factor> | <Number> | '(' <Expr> ')'
<Number> ::= <Digit> {<Digit>} [<Separator> <Digit> {<Digit>}]
 [<Exponent> [<Sign>] <Digit> {<Digit>}]
<UnaryOp> ::= '+' | '-'
Здесь опущены определения некоторых вспомогательных символов, которые не изменились.
Мы видим, что грамматика стала "более рекурсивной", т.е. в определении символа <Factor> используется он сам. Соответственно, функция Factor будет вызывать саму себя.
Символ <UnaryOp>, определение которого совпадает с определениями <Operator1> и <Sign>, мы делаем независимым нетерминальным символом по тем же причинам, что и ранее: в принципе, синтаксис может допускать унарные операции (как, например, not в Delphi), которые не являются ни знаками, ни допустимыми бинарными операциями.
Побочным эффектом нашей грамматики стало то, что, например, -5 воспринимается как множитель, а потому перед ним допустимо поставить унарный оператор, т. е. выражение --5 также является корректным множителем и трактуется как -(-5). А перед --5, в свою очередь, можно поставить еще один унарный оператор. И так — до бесконечности. Это может показаться не совсем правильным, но, тем не менее, такая грамматика широко распространена. Легко, например, убедиться, что компилятор Delphi считает допустимым выражение 2+-+-2, трактуя его как 2+(-(+(-2))). Листинг 4.8 иллюстрирует реализацию данной грамматики.
Листинг 4.8. Реализация калькулятора со скобками
// Так как грамматика рекурсивна, функция Expr
// должна быть объявлена заранее
function Expr(const S: string; var Р: Integer): Extended; forward;

 

// Выделение подстроки, соответствующей <Factor>,
// и ее вычисление
function Factor(const S: string; var P: Integer): Extended;
begin
 if P > Length(S) then
  raise ESyntaxError.Create('Неожиданный конец строки');
 // По первому символу подстроки определяем,
 // какой это множитель
 case S[Р] of
 '+': // унарный "+"
 begin
  Inc(Р);
  Result := Factor(S, P);
 end;
 '-': // унарный "-"
 begin
  Inc(P);
  Result := -Factor(S, P);
 end;
 '(': // выражение в скобках
 begin
  Inc(P);
  Result := Expr(S, P);
  // Проверяем, что скобка закрыта
  if (Р > Length(S)) or (S[P] <> ')') then
   raise ESyntaxError.Create(
    'Ожидается ")" в позиции ' + IntToStr(P));
  Inc(P);
 end;
 '0'..'9': // Числовая константа
  Result := Number(S, P);
 else
  raise ESyntaxError.Create(
   'Некорректный символ в позиции ' + IntToStr(Р));
 end;
end;

 

// Выделение подстроки, соответствующей <Term>,
// и ее вычисление
function Term(const S: string; var P: Integer): Extended;
var
 OpSymb: Char;
begin
 Result := Factor(S, P);
 while (P <= Length(S)) and IsOperator2(S[P]) do
 begin
  OpSymb := S[P];
  Inc(P);
  case OpSymb of
  '*': Result := Result * Factor(S, P);
  '/': Result := Result / Factor(S, P);
  end;
 end;
end;

 

// Выделение подстроки, соответствующей <Expr>,
// и ее вычисление
function Expr(const S: string; var Р: Integer): Extended;
var
 OpSymb: Char;
begin
 Result := Term(S, P);
 while (P <= Length(S)) and IsOperator1(S[P]) do
 begin
OpSymb := S[P];
  Inc(P);
  case OpSymb of
  '+': Result := Result + Term(S, P);
  '-': Result := Result - Term(S, P);
  end;
 end;
end;

 

// Вычисление выражения
function Calculate(const S: string): Extended;
var
 P: Integer;
begin
 P := 1;
 Result := Expr(S, P);
 if P <= Length(S) then
  raise ESyntaxError.Create(
   'Некорректный символ в позиции ' + IntToStr(Р));
end;
По сравнению с предыдущим примером функция Term осталась такой же с точностью до замены вызовов Number на новую функцию Factor. Функция Factor выделяет подстроку, отвечающую отдельному множителю. Множители, напомним, могут быть трех типов: число, выражение в скобках, множитель с унарным оператором. Различить их можно по первому символу подстроки. Функция Factor распознает тип множителя и вызывает соответствующую функцию для его вычисления.
Функция Expr теперь может применяться не только к выражению в целом, но и к отдельной подстроке. Поэтому она, как и все остальные функции, теперь имеет параметр-переменную P, через который передается начало и конец этой подстроки. Из функции убрана проверка того, что в результате ее использования строка проанализирована полностью, т.к. теперь допустим анализ части строки.
Функция Expr в своем новом виде стала не очень удобной для конечного пользователя, поэтому была описана еще одна функция — Calculate. Это вспомогательная функция, которая избавляет пользователя от вникания в детали "внутренней кухни" калькулятора, т.е. использования переменной P и проверки того, что строка проанализирована до конца.
Пример калькулятора со скобками записан на компакт-диске под названием BracketsCalcSample. Анализируя его код, можно заметить, что по сравнению с предыдущим примером незначительно изменена функция Number — из нее в соответствии с новой грамматикой убрана проверка знака в начале выражения.
Назад: 4.5. Учет приоритета операторов
Дальше: 4.7. Полноценный калькулятор