Парсинг математических выражений AngouriMath, DynamicExpresso и MathExpressionEval

Пожалуй большинство программистов так или иначе сталкивались с с++. Как и я большинство таких людей начинали познавать с++ через его великого основателя Бьярне Страуструпа. Одна из его энциклопедий по с++ начиналась с калькулятора. С подобной задачей написания калькулятора для расчета выражений нам довелось столкнуться в рамках одного из спринтов только уже на c#.

В конечном итоге задача сводилась к массовому расчету строковых математических выражений с возможностью использовать переменные и дальнейшим развитием в сторону кастомных функций. Поскольку частично задача состояла в рефакторинге уже существующего кода и реализации новых задач, подобную задачу уже решали с помощью roslyn. Однако мы решили уйти от этого решения ввиду возможной инъекции кода и медленной скорости работы. 

Так, осматривая просторы интернета, мы наткнулись на несколько интересных возможных аналогов, некоторые из которых мы сейчас рассмотрим:

  1. AngouriMath

  2. DynamicExpresso

  3. MathExpressionEval

AngouriMath

Свободная open-source библиотека символьной алгебры. Библиотека с хорошим описанием, имеющая свой сайт с подробным описание возможностей использования и настроек парсинга строковых выражений. Работать с данной библиотекой было легко и достаточно удобно вот некоторые примеры:

Entity a = "3+4+5";
Entity a = "3x+4y+5z";

такое привидение под капотом приведет к вызову MathS.FromString (»3x+4y+5z»). На выходе мы получим некоторый результат в виде структуры Entity если выражение было распознано или UnhandledParseException, если распознать не удалось. Сам по себе класс Entity напоминает элемент дерева выражений:

Entity a =

Entity a = »3+4+5»

Подробнее можно прочитать в документации.

Если работать с исключениями не удобно, или хочется подробнее узнать причину невозможности парсинга можно напрямую вызвать: MathS.Parse результат работы этой функции в отличии от MathS.FromString возвращающий Either от Entity и причины неудачи. 

Сама же по себе библиотека многогранна и парсинг является только одной из граней, хотя и весьма приятной. Однако, не обошлось и без ложки дегтя в случае самого парсинга так, например выражение типа:

MathS.Parse("23*x3")

будет распознано не как 23 * x3, где x3 — переменная, выражение вернет результат:

результат выполнения MathS.Parse(

результат выполнения MathS.Parse (»23*x3»)

где x^3 — это x в 3 степени, что, разумеется, не корректно.

DynamicExpresso

Данная библиотека работает как интерпретатор простых операторов C#, написанных в .NET Standard 2.0. Dynamic Expresso внедряет собственную логику синтаксического анализа и фактически интерпретирует операторы C#, преобразуя их в лямбда-выражения или делегаты .NET.

схема работы библиотеки

схема работы библиотеки

Пример работы данного парсера также прост:

var _interpreter = new Interpreter();
var result = _interpreter.Eval("3+4+5");

здесь создается некий класс Interpreter и получается значение выражения. Такой вызов под копотом вызовет метод парсера ParseAssignment, который будет разделять выражение по дереву выражений, разделяя их по операциям, часть из которых представлена в enum TokenId. Затем, когда дерево выражений окончательно разобьется, по шаблону посетитель будет выполняться проверка соединение выражений и переменных в окончательное дерево, которое вместе с параметрами завернется во внутренний класс Lambda,

ce0fe965f42f3ec779da78e7ab28a840.png

а затем вызовется метод Invoke с параметрами, для применения их значений. Здесь стоит понимать разницу между переменными и параметрами, переменные (Variables) применяются на уровне соединения интерпретатора, а параметры (Parameters) на уровне получения значений, как следствие, после вызова метода Parse мы получим применение переменных, но не параметров. Если же выражение распарсить не удается мы получим UnknownIdentifierException

Библиотека больше ориентирована под c#, чем под математические выражение и позволяет парсить, выполнять и получать значения многих конструкций самого языка c#, что делает ее достаточно удобной и многогранной в использовании. 

MathExpressionEval

Наверное, самая легкая, быстрая и простая в освоении библиотека. Библиотека оценки математических выражений для c#. Данная библиотека позволяет оценивать математические выражения, логические выражения, строковые выражения и выражения даты и времени. Пример работы:

var expression = new Expression("3+4+5");
var result = expression.Eval()

если первая строка только создает выражение, которое просто определяет контекст, парсер, строковое выражение и прочее, то вторая строчка выполняет полноценный парсинг строкового выражения, получая полноценное дерево выражений и рассчитывает его значение.

var expression = new Expression(

var expression = new Expression (»3+4+5») (после выполнения Eval ())

В случае невозможности парсинга выпадет Exception, однако есть возможность проверить возможность парсинга с помощью вызова метода GetError класса Expression.

Применение переменных, параметров и констант здесь не отличатся и работает вызовом метода Bind у класса Expression. Можно отключать набор определенных функций.

В целом же данная библиотека простая, легкая, быстрая и достаточно прозрачная для использования.

Сравнение и итоги

Данные библиотеки полностью обеспечивают решение вышепоставленных задач. Несмотря на то, что все они работают похожим образом у каждой из них есть свои специфические решения, которые стоит учитывать при использовании. Ниже будет приведена таблица для сравнения результатов работы, на основании которой будет делаться окончательный выбор.

Библиотека

Количество операций в формуле

Среднее время работы, нс

Аллокации памяти, B

AngouriMath

10

310.8

336

30

1201.5

1072

50

2301.2

1488

DynamicExpresso

10

1420

262

30

4113

834

50

6772

1406

MathExpressionEval

10

479

299

30

1387

933

50

2319

1573

Отличные результаты были получены от AngouriMath и MathExpressionEval, на самом деле тестов, конечно же было больше, но данная таблица иллюстрирует, что первая и третья библиотеки лучше справляются с поставленной задачей с примерно одинаковыми затратами, с учетом несколько не удобной работы с переменными мы сделали выбор в пользу MathExpressionEval.

Однако, как оказалось, каждая из библиотек была полезна в своем роде и для своих задач, но это уже совсем другая история.

Репозиторий с тестами: https://github.com/alexSmite1993/EvalAppTest

© Habrahabr.ru