Pull to refresh

Отвёртка для выражений

Reading time5 min
Views3.2K
Область применения разбора математических выражений представить не сложно — это и всевозможные парсеры SQL-запросов, и обработчики формул, вводимых пользователем (то же построение графиков или фильтры к БД) — вплоть до создания собственных языков (намеренно не пишу слово «программирования», т.к. зачастую это языки описания данных и иже с ними).

Возможно, я не прав, но я не сумел найти на просторах сети более или менее юзабельный парсер выражений для PHP — и, как наверное уже привыкли те, кто периодически читает мои статьи, я отправился реализовывать это дело своими силами, т.е. изобретать велосипед. :^)

Результат моих потуг вы можете обнаружить здесь. В архиве вы найдёте скрипты, необходимые для функционирования библиотеки, и пример её работы (sample.php). Библиотека собрана как standalone.

Но, я полагаю, интересно было бы разобраться, что там к чему.

Как несложно догадаться, библиотека построена на основе ОПЗ (обратная польская запись, или нотация, если вам так привычнее). Однако ОПЗ имело смысл доработать до нужной кондиции, потому как в её описании даётся лишь базовый функционал для разбора выражений. Итак, вот схема работы библиотеки:

Описание правил разбора выражения производится в класса, унаследованном от класса CIpr. Определение грамматик и классов производится в защищённом методе p_build.

Для начала нужно определить грамматические классы (или грамматики), на основе которых парсер будет разделять переданную ему строку на лексемы. Каждая лексема состоит из токена (собственно слова) и идентификатора класса (того, который вызвал выделение этой лексемы). Идентификатор класса — это индекс массива, т.е. это может быть строка или целое число (я использовал строки для наглядности). Я определил следующие возможные типы грамматик:

grammar_char(символы[, сочетания])
Такие грамматики определяются набором символов (строка, в которой эти символы перечислены) и, возможно, сочетаниями символов (массив строк).
Определение выглядит так: grammar_char("+-*/=", array("+=", "++")). Эта грамматика определяет одиночные символы +, -, *, / и = и их сочетания += и ++.

grammar_list(лексемы[, чувствительность к регистру = true])
Этот тип грамматик определяется предопределёнными наборами лексем. Можно так же определить, учитывать ли при определении лексемы регистр (по умолчанию — да).
Использовать можно так: grammar_list(array(«prefix1», «prefix2»), false). Эта грамматика определяет лексемы prefix1 и prefix2, не чувствительные к регистру. Важно, что будет просто производиться поиск одной лексемы из списка в начале строки без учёта разделителей, т.е. из выражения prefix1something будет выделена лексема prefix1.

grammar_preg(начло[, извлечение])
Этот тип грамматик, вместе с символьными, я думаю будет наиболее полезен. Это грамматики, построенные на основе регулярных выражений. Они описываются регуляркой начала и, возможно, регуляркой для извлечения. Если извлечение не указано, то вместо него используется начало. Вот такое регулярное выражение, например, выберет комментарии вида // комментарий: /^\/\/.*/

grammar_quot(символ кавычки[, режим экранирования = C_ESCAPE_CHAR])
Грамматики этого типа — это заключённые в кавычки строки. Режим экранирования может принимать следующие значения:
C_ESCAPE_NONE — экранирование символов в строке не производится. (Строка «10\»" будет распознана как 10\).
C_ESCAPE_CHAR — экранировать можно только управляющие символы. (Строка «10\'-\»" будет распознана как 10\'-").
C_ESCAPE_ALL — экранирующий символ \ можно ставить перед всеми символами. (Строка «1\0\'-\»" будет распознана как 10'-").

grammar_brac(символы скобок[, режим экранирования = C_ESCAPE_ALL[, <разрешать вложенность = true>]])
Это скобочные грамматики (т.е. выделяющие выражения типа <anfg>). Их использование лично мне представляется сомнительным в данном контексте — они предназначены для других целей (эта библиотека — только часть платформы). Но можете их использовать, если пригодятся. :^)

grammar_user(начало, извлечение)
Это пользовательская грамматика, определяемая функциями начала и извлечения.

Грамматический разбор происходит следующим образом: Поданная на вход строка поочерёдно проверяется всеми грамматиками, и если обнаружено начало одной из них (как правило, совпадающее с началом строки), то производится её извлечение. В результате грамматического разбора вы получите массив лексем.

Дальше, что интереснее, определяются классы возможных команд в коде выражения. Можно определять следующие типы команд:

ipr_lexeme(лексема)
Определяет, что указанная лексема должна обрабатываться как команда типа «лексема». В роли лексемы может выступать как полноценная лексема, определённая через функции lexeme и ilexeme, или просто строка, определяющая токен лексемы.

ipr_operator(лексема, приоритет[, арность = 2[, ассоциативность = нет(что сейчас эквивалентно левой, что не совсем верно)]])
Определяет оператор.
Можно так же использовать функции:
ipr_perfix — префиксный унарный оператор;
ipr_postfix — постфиксный унарный оператор;
ipr_binary — бинарный оператор.

ipr_compound(массив лексем, приоритет[, арность = 3[, ассоциативность = правая[, вычислять автоматически = нет]]])
Определяет сложный оператор. В чём отличие от простого? Операнды простого оператора вычисляются самим движком, что, скажем, для оператора ?: не совсем корректно, т.к. вычислить нужно только один из его второго и третьего операндов. Для этого и нужен сложный оператор — для него вычисляется автоматически только первый операнд, а все последующие передаются на обработку в пользовательский метод. Таким образом имеет смысл определять сложные операторы с арность не менее 2.
Можно так же использовать функции:
ipr_access — бинарный оператор доступа к полю объекта;
ipr_ternary — тернарный оператор.

ipr_brackets(открывающая лексема, закрывающая лексема, запятая[, могут ли использоваться без операнда = нет[, арность = не определено[, вычислять автоматически = да]]])
Определяет скобки. Скобки могут использоваться без операнда (например (a + b)) или с операндом (например, arr[10]). Так же может быть определена арность скобок, когда в них нужно указывать строго определённое количество операндов. Про автоматический можно прочитать выше.
Можно так же использовать функции:
ipr_default — определение скобок, используемых для установки приоритете (т.е. скобок по умолчанию).

ipr_instruction(лексема, открывающая лексема, закрывающая лексема, запятая[, арность = не определено[, вычислять автоматически = нет]])
Определяет инструкцию. Фактически то же самое, что и скобки с запретом автоиспользования, только вместо операнда перед скобками используется лексема инструкции.

ipr_end(лексема)
Определяет лексему, на которой выражение считается закончившимся.

ipr_until(лексема)
Определяет лексему, на которой выражение считается закончившимся. При этом лексема остаётся в начале остатка выражения.

Теперь нужно определить обработчики всех типов команд, коих 5 — операнд, лексема, оператор, сложный оператор, скобки и инструкция. Это следующие защищённые методы:

p_run_operand(команда)
p_run_lexeme(команда)
p_run_operator(команда, операнды)
p_run_compound(команда, операнд, операнды)
p_run_brackets(команда, операнд, операнды)
p_run_instruction(команда, операнды)

Команда содержит тип команды (не важен, т.к. им и определяется обработчик, т.е. ситуация, когда в обработчик операнда придёт команда оператора, невозможна), класс команды и лексему, которая была определена как команда. Операнд содержит вычисленный операнд, а операнды — операнды, либо вычисленные, либо ожидающие обработки через метод p_run (в зависимости от типа команды и флага «вычислять автоматически»).

Теперь, чтобы создать обработчик выражения, нужно создать экземпляр класса обработчика, которому передано в конструктор выражение для обработки. Конструктор выполнит грамматический разбор и компиляцию выражения. Чтобы запустить выражение, следует использовать метод run.

Ну вот вроде бы и всё. Сейчас в пути обработка ошибок и неассоциативные операторы — надеюсь, я всё это доделаю в ближайшее время. Как обычно, прошу не быть слишком строгими тех, кому это уже не вновьё — я публикую свои разработки для тех, кому они могут пригодиться, ведь разработки, мне кажется, не совсем бесполезные.

Благодарю за ваше внимание! Буду очень рад услышать ваши отзывы и замечания в комментариях вне зависимости от их положительности. :^)
Tags:
Hubs:
+4
Comments17

Articles

Change theme settings