Как стать автором
Обновить

Разработка расширяемого алгоритма строкового калькулятора

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров2.6K
Всего голосов 4: ↑4 и ↓0+4
Комментарии11

Комментарии 11

Много операций со строками. Выглядит не особо быстрым. Когда-то делал такое, но на стеке: объединил алгоритм вычисления обратной польской нотации и алгоритм преобразования из строки в обратную польскую нотацию, причём так, чтобы всё это было однопроходным.

А так, что бы ещё добавил,так это функции от нескольких аргументов. Там у вас по сути небольшая доработка будет: указываем у функции, сколько аргументов она хочет, далее из списка забираем их.

На первом курсе тоже делал нечто подобное. Для построения обратной польской нотации из инфиксной нотации использовал Алгоритм Сортировочной Станции (Shunting Yard Algorithm) от Дейкстры.

я так же делала только на C#, в этот раз мне хотелась как то иначе, ну и цели в этот раз иные были и мне отчасти хотялась поупряжнятся еще на Scala писать, мой основной ЯП большую часть времени был Java

Тоже делал подобную штуку в студенческие годы. По строке строилось дерево выражения, которое потом вычислялось (помимо представленных тут BinaryOperation/UnaryFunction/Constant, была ещё сущность Variable, которая по сути переменная и брала своё значение по имени из коллекции переданных параметров). Так же ради интереса добавил символьное дифференцирование и тривиальные упрощения.

в моем вариант можно дифференцирование легко добавить)
addUnaryFunction(createUnary("d", num => (num+0.00000001)/0.00000001))

Эм... Ну как бы подобные задачи обычно решают иначе, например вот так. На строках неудобно, длинно и чревато ошибками, а тут конечный автомат и стек.

По строке строилось дерево выражения, которое потом вычислялось (помимо представленных тут BinaryOperation/UnaryFunction/Constant, была ещё сущность Variable, которая по сути переменная и брала своё значение по имени из коллекции переданных параметров
Это правильный подход, поскольку для решения практических задач приходится достаточно часто вычислять значение одного и того же выражения множество раз (например, при построении графиков функций, вычислении определенных интегралов, численном решении дифференциальных уравнений)
в моем вариант можно дифференцирование легко добавить)
addUnaryFunction(createUnary(«d», num => (num+0.00000001)/0.00000001))
Не очень понял, как это вы добавляете дифференцирование, похоже, вы пытаетесь вычислять производную численно, да еще и прямо по определению (это не слишком эффективно). Конечно же, в нормальном калькуляторе, если уж он умеет вычислять значения выражений, должно (ладно, не должно, но не помешает) присутствовать аналитическое дифференцирование, т.е. построение по заданному выражению выражения для его производной по заданной переменной (эту операцию лучше применять сразу к синтаксическому дереву выражения)

построение по заданному выражению выражения для его производной по заданной переменной (эту операцию лучше применять сразу к синтаксическому дереву выражения)

Именно. По правилам дифференцирования. Плюс уборка всякого "мусора" (вроде умножения на 1 или 0, сложения с нулем, и т.д.), который остается от чисто механического применения этих правил. А в идеале, как следует сокращать выражение, это уже куда более хитрая задача (например, выражение (2x + sin(x)^2 - x + cos(x)^2 - x) сокращается до 1, но слагаемые могут быть "неудачно" раскиданы по узлам BinaryOperation, и надо их как-то "сводить" друг с другом).

Плюс уборка всякого «мусора» (вроде умножения на 1 или 0, сложения с нулем, и т.д.), который остается от чисто механического применения этих правил

Упрощение промежуточных выражений при аналитическом дифференцировании
image

Зашёл почитать про лексеры и парсеры, и, хоть я и не Scala-разработчик, имею много замечаний по прочитанному коду.

Первое - в BinaryOperation/UnaryFunction не надо делать equals(), который принимает String. Гораздо лучше будет для хранения операций использовать не List, а Map (она же есть в Scala, я верю в это), тем более что вы потом по содержимому List-а делаете операции map()/filter().

Во-вторых, и бинарные операции, и унарные, и константы имеют очень много общего: они имеют обозначение (ваше getDesignation), и могут быть вычислены (ваше calc()/getValue), поэтому вместо того, чтобы объявлять эти методы в трейтах по-отдельности, вполне подошёл бы родительский трейт (я верю, что такое есть в Scala). При этом сильно убавится кода, который делает одно и то же для разных трейтов (например, токенизация).

В-третьих, нафига BinaryOperactionFabric? Вместо того, чтобы императивно клеить символ к функции и кешировать, проще в коде декларативно наобъявлять статических экземпляров трейта, а "фабрика" по-английски будет "Factory".

Тут я, наконец, добрался до токенизации. Ну, это где символы обрамляются сепараторами через string.replace(), и потом всё нарезается через string.split(). Мощно. Я долго искал себе оправдание, почему я не додумался до такого? Потом придумал искусственный случай, когда это сломается (если обозначение операции является подстрокой обозначения другой операции), и успокоился.

А где же парсер? А его тут нет. Тут сразу считалка, которая выполняет операции в том порядке, в котором они объявлены. Прикольно, если бы токенизатор понимал пробел как разделитель, то считалка понимала бы сразу и префиксную нотацию, и инфиксную, и постфиксную! (И любую кашу из них).

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

Я вообще не хотел насмехаться ни над кодом, ни над автором (хотя скорее всего звучит именно так), я хотел мозг свой размять.

На питоне (как и всё другое) калькулятор делается в одну сточку print(eval(input())).

На крайний случай существуют peg generator.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории