На этом шаге рассмотрим синтаксический анализ формул в приложении Электронная таблица.
Функция evalExpression() возвращает значение выражения из ячейки электронной таблицы. Выражение состоит из одного или нескольких термов, разделенных знаками операций "+" или "-". Термы состоят из одного или нескольких факторов, разделенных знаками операций "*" или "/". Разбивая выражения на термы, а термы на факторы, мы обеспечиваем правильную последовательность выполнения операций.
Например, "2*C5+D6" является выражением, первый терм которого будет "2*С5", а второй терм - "D6". "2*С5" является термом, первый фактор которого будет "2", а второй фактор - "С5"; "D6" состоит из одного фактора - "D6".
Фактором может быть число ("2"), обозначение ячейки ("С5") или выражение в скобках, перед которым может стоять знак минуса. Блок-схема синтаксического анализа выражений электронной таблицы представлена на рис. 1.
Рис.1. Блок-схема синтаксического анализа выражений электронной таблицы
Для каждого грамматического символа (выражение, терм и фактор) имеется соответствующая функция-член, которая выполняет его синтаксический анализ и структура которой очень хорошо отражает его грамматику. Построенные таким образом синтаксические анализаторы называются парсерами с рекурсивным спуском (recursive-descent parsers).
QVariant Cell::evalExpression(const QString &str, int &pos) const { //вызываем функцию evalTerm() для получения значения первого терма QVariant result = evalTerm(str, pos); while (str[pos] != QChar::Null) { QChar op = str[pos]; /*если за ним не идет символ "+" или "-", то выражение состоит из единственного терма, и мы возвращаем его значение в качестве значения всего выражения*/ if (op != '+' && op != '-') return result; ++pos; //в противном случае, вызываем второй раз evalTerm() QVariant term = evalTerm(str, pos); /*после получения значений первых двух термов мы вычисляем результат операции в зависимости от оператора. Если при оценке обоих термов их значения будут иметь тип double, мы рассчитываем результат в виде числа типа double; в противном случае мы устанавливаем результат на значение Invalid*/ if (result.type() == QVariant::Double && term.type() == QVariant::Double) { if (op == '+') { result = result.toDouble() + term.toDouble(); } else { result = result.toDouble() - term.toDouble(); } } else { result = Invalid; } } return result; }
Продолжаем эту процедуру, пока не закончатся термы. Это даст правильный результат, потому что операции сложения и вычитания обладают свойством "ассоциативности слева" (left-associative); то есть "1-2-3" означает "(1-2)-3", а не "1-(2-3)".
Функция evalTerm() очень напоминает функцию evalExpression(), но в отличие от последней она имеет дело с операциями умножения и деления.
QVariant Cell::evalTerm(const QString &str, int &pos) const { QVariant result = evalFactor(str, pos); while (str[pos] != QChar::Null) { QChar op = str[pos]; if (op != '*' && op != '/') return result; ++pos; QVariant factor = evalFactor(str, pos); if (result.type() == QVariant::Double && factor.type() == QVariant::Double) { if (op == '*') { result = result.toDouble() * factor.toDouble(); } else { if (factor.toDouble() == 0.0) { result = Invalid; } else { result = result.toDouble() / factor.toDouble(); } } } else { result = Invalid; } } return result; }
В функции evalTerm() необходимо учитывать одну тонкость, а именно, нельзя допускать деление на нуль, так как это приводит к ошибке на некоторых процессорах. Хотя не рекомендуется проверять равенство чисел с плавающей точкой из-за ошибки округления, можно спокойно делать проверку на равенство значению 0.0 для предотвращения деления на нуль.
Функция evalFactor():
QVariant Cell::evalFactor(const QString &str, int &pos) const { QVariant result; bool negative = false; //начинаем с проверки, не является ли фактор отрицательным if (str[pos] == '-') { negative = true; ++pos; } //проверяем наличие открытой скобки if (str[pos] == '(') { ++pos; /*если она имеется, анализируем значение внутри скобок как выражение, вызывая evalExpression(). При анализе выражения в скобках evalExpression() вызывает функцию evalTerm(), которая вызывает функцию evalFactor(), которая вновь вызывает функцию evalExpression(). Именно в этом месте осуществляется рекурсия при синтаксическом анализе*/ result = evalExpression(str, pos); if (str[pos] != ')') result = Invalid; ++pos; } else { /*если фактором не является вложенное выражение, мы выделяем следующую лексему (token), и она должна задавать обозначение ячейки или быть числом*/ QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}"); QString token; while (str[pos].isLetterOrNumber() || str[pos] == '.') { token += str[pos]; ++pos; } /*если эта лексема удовлетворяет регулярному выражению в переменной QRegExp, мы считаем, что она является ссылкой на ячейку, и вызываем функцию value() для этой ячейки. Ячейка может располагаться в любом месте в электронной таблице, и она может ссылаться на другие ячейки. Такая зависимость не вызывает проблемы и просто приводит к дополнительным вызовам функции value() и к дополнительному синтаксическому анализу ячеек с признаком "dirty" для перерасчета значений всех зависимых ячеек*/ if (regExp.exactMatch(token)) { int column = token[0].toUpper().unicode() - 'A'; int row = token.mid(1).toInt() - 1; Cell *c = static_cast<Cell *>( tableWidget()->item(row, column)); if (c) { result = c->value(); } else { result = 0.0; } } else { //если лексема не является ссылкой на ячейку, рассматриваем ее как число bool ok; result = token.toDouble(&ok); if (!ok) result = Invalid; } } if (negative) { if (result.type() == QVariant::Double) { result = -result.toDouble(); } else { result = Invalid; } } return result; }
Что произойдет, если ячейка А1 содержит формулу "=А1"? Или если ячейка А1 содержит "=А2", а ячейка А2 содержит "=А1"? Хотя нами не написан специальный программный код для обнаружения бесконечных циклов в рекурсивных зависимостях, парсер прекрасно справится с этой ситуацией и возвратит недопустимое значение переменной типа QVariant. Это даст нужный результат, поскольку мы устанавливаем флажок cachelsDirty на значение false и переменную cachedValue на значение Invalid в функции value() перед вызовом evalExpression(). Если evalExpression() рекурсивно вызывает функцию value() для той же ячейки, она немедленно возвращает значение Invalid, и тогда все выражение принимает значение Invalid.
Теперь мы завершили программу синтаксического анализа формул. Ее можно легко модифицировать для обработки стандартных функций электронной таблицы, например "sum()" и "avg()", расширяя грамматическое определение фактора. Можно также легко расширить эту реализацию, обеспечив возможность выполнения операции "+" над строковыми операндами (для их конкатенации); это не потребует внесения изменений в грамматику.
Файлы приложения Электронная таблица можно взять здесь.
На следующем шаге приступим к рассмотрению большого раздела, посвященного компьютерной графике, и начнем с классов геометрии.