У меня калькулятор.
Я делал с размахом в 2 этапа:
1. Парсинг на токены - объекты с общим наследником.
2. Сортировка в постфиксную форму, и одновременно, проверка правильности порядка.
Так как у меня котлин, я сделал в полу-функциональном стиле (анонимные функции с одинаковой сигнатурой в хеш-таблице), но можно просто через switch или if вызывать приватные классы метода.
Сперва парсинг.
Суть вот в чем: есть объект с состоянием. Там только поля, и вроде-бы еще был какой-то метод. Короче, заменитель struct'а.
И функции. Они принимают состояние, текущий символ и следующий символ. И типы:
Есть унарные операторы, числа, открывающие скобочки, закрывающие скобочки. Функции я отнёс к типа-унарным операторам. Когда уже идет работа с токенами, я часто писал (функция или унарный оператор)
. Ну похоже же:
sqrt(16)
и -16
Из-за скобочек пришлось выносить функции в отдельный тип.
Ну так вот. В объекте есть стек для завершенных токенов, есть буфер для символов, чтобы по символам добавлять название функции или многозначное число, есть булевый флаг того, оператор ли сейчас или операнд, есть флаг того, была ли точка в числе. Это для борьбы с числами типа 23.345.3.456.
Глобальный цикл итерируется по символам, скидывает его функции, которая возвращает тип (я не помню, этой функции вроде бы нужен объект состояния), по типу получает соответствующую функцию, скидывает ей объект, текущий символ и следующий символ.
Если весь парсинг делать в одном классе с методами, то объект состояния не нужен, просто свойства в парсере. Я сперва так сделал, потом переделал на независимые функции.
Теперь самая главная вещь в алгоритме - это флаг оператор/операнд. Функция проверки говорит что текущий символ "+" - это бинарный оператор, но флаг стоит в положении операнд - функция для бинарных операторов выбрасывает исключение. И т.д.
У каждого типа своя маленькая функция. Например открывающая скобочка (она соответствует операнду):
Если сейчас оперетор:
Выкинуть исключение
Иначе:
Добавить OpenBracketToken() в стек.
Или вот бинарный оператор:
Если сейчас оператор:
Добавить BinaryOperatorToken(symbol) в стек
Переключить в операнд
Иначе:
Выкинуть исключение
(два бинарных оператора не могут идти подряд). С помощью такого простого флага мы невзначай отсеиваем половину невалидных выражений, еще можем отличить унарный минус от бинарного.
Короче, как-то так. У функций и чисел проверяем следующий символ, если конец, выгребаем буфер в строку, потом в число если число, потом в токен. Если точка уже была (смотрим флаг), но появилась снова, выбрасываем исключение. И если следующий символ - не закрывающая скобочка, переключаем в с операнда в оператор, закрывающая скобочка тоже переключает в оператор если после нее не идет еще одна закрываюшая скобочка. Пробелы нужно пропускать. Думаю, все варианты сам разберешь. В конце вываливаем стек в массив.
В начало я кладу StartToken, в конец EndToken.
Итого у нас выходит вот такое. Из:
(sqrt(5) * 7)
Вот это (в котлине не нужен new):
[StartToken(), FunctionToken("sqrt"), OpenBracketToken(), NumberToken(5.0), CloseBracketToken(), BinaryOperatorToken('*'), NumberToken(7.0), CloseBracketToken(), EndToken()]
И вот содержимое части, занимающейся парсингом:
Класс с состоянием {
Парочка
Полей
}
Хеш-Таблица {
Тип : Функция,
Тип : Функция,
Тип : Функция
}
Функция главного цикла { }
Проверка типа { }
А переделать можно вот так:
Класс парсинг {
Поле раз
Поле два
Поле три
Главный цикл { }
Получить тип { }
Функция типа A { }
Функция типа Б { }
Функция типа В { }
}
Все, конечно же приватно.
Потом сортировка в постфикс, но это уже другая история, да и гуглится легко. Только мне пришлось еще придумать как обрабатывать функции (как унарный оператор, но после него обязательна скобочка).
Отдельная история - запятая аргументов функции.
Так как токены у нас - это классы, то мы можем запихать проверки вводимых данных, еще мне пришлось перегрузить операторы сравнения, чтобы NumberToken(5.0) был равен другому NumberToken(5.0).
Короче, думать пришлось много. И парочка костылей п