Аналізатор математичних виразів C# — досвід дилетанта

Комп'ютер і людина — як складно нам зрозуміти один одного. По суті, процес програмування — це пояснення машині те, що ти від неї хочеш на зрозумілій їй мові.
введення
По своїй роботі, так і в якості хобі, я пов'язаний з процесом написання коду, пов'язаного з математичними обчисленнями. Однією з останніх завдань було написання ПЗ, в якому користувач мав би можливість самостійно вводити і використовувати при розрахунку, візуалізації даних і оптимізації деяких математичних виразів. А враховуючи свою природну лінь і небажання постійно доповнювати бібліотеки коду спеціальних математичних функцій прийшла в голову думка — а чому б не реалізувати маячну студентську ідею, і не винайти велосипед парсера математичних виразів.

Звичайно, перш ніж взятися за винахідницький процес (знову ж, на увазі вселенської ліні), було достатньо довгий зґвалтування Яндекса і Гугла на предмет вже існуючих реалізацій. І їх знайшлося звичайно ж не мало. Але, на жаль, того, чого хотілося домогтися від конкретної реалізації не знайшлося. А критерії пошуку були наступні:

  • Парсер повинен бути реалізований під .NET не старше 4.0;
  • Він повинен обробляти всі базові математичні оператори (+,-,*,/,^, тощо) з урахуванням їх пріоритетів і дужок різних видів;
  • Він повинен розпізнавати основні функції (на зразок sin, cos), мати змогу додавати створений об'єкт парсера свої функції із зазначенням делегатів методів, вычисляющих їх значення для будь-якої кількості вхідних змінних);
  • Повинна бути можливість використання відомих парсеру констант, і додати їх у список, використовуваний при розборі вираження;
  • Повинен бути присутнім механізм роботи з параметрами та змінними. При цьому потрібні змінні різних типів: просто зберігають числове значення, або викликають подія, в якому зовнішній код визначає їх значення на момент їх виклику;
  • Повинен бути реалізований механізм функціоналів (мінімум — інтегрування, сума ряду і диференціювання)
  • Результат розбору рядкового виразу повинен бути представлений в об'єктній моделі бінарного дерева;
  • найголовніше — бінарне дерево повинно мати можливість відображення на дерево Linq.Expression з подальшою компіляцією його делегат, виконує обчислення швидкості самої платформи .NET.

Мета велосипедостроения
Власне, головна мета винаходу велосипеда була в можливості компіляції рядка мат.вирази делегат з високою швидкістю виконання процесу розрахунку значення.

На жаль мої мізерні здатності до роботи з пошуковими системами, відсутність часу, сил і лінь не дали позитивного результату в пошуку прототипу і було прийнято рішення пуститися в подорож по граблях.

Модель і основна ідея
Об'єктна модель являє собою два класи: клас парсера і клас математичного виразу. Всередині використовується ще три додаткових класи: клас дерева математичного виразу, абстрактний клас вузла дерева мат.вирази й клас логічного елемента рядка мат.вираження — терма. Комі того, реалізовані класи функції, змінної, функціоналу і, відповідно, колекцій функцій, змінних і функціоналів (вони вкладені в клас мат.вирази).

Ідея, яка не претендує на відкриття, або оптимальність рішення, але була спробою підступитися до вирішення завдання, полягала в тому, що б розбити вихідну послідовність символів рядка мат.вирази на деякі логічні складові. Сформувати ієрархічну послідовність введених логічних блоків мат.вирази. І на її основі побудувати дерево мат.вирази.

Проектування почалося з ідеї — «а як би я хотів, що б це виглядало в завершеному вигляді, і що б це було легко і зручно використовувати?». Хотілося реалізувати наступний сценарій використання:

Створюється об'єкт парсера, який закріплюється де-небудь на рівні моделі подання, або в бізнес-логікою. Він конфігурується: до нього додаються необхідні константи, що визначаються функції, з якими він повинен надалі працювати. Додаються передплатники подій для обробки невідомих функцій і змінних. І парсер чекає виклику методу Parce(string).

У основний метод Parce() передається у якості вхідних даних рядок матвыражения, а його результатом є об'єкт мат.вирази, що містить дерево мат.вирази.

У об'єкта мат.вирази повинні бути представлені колекції функцій, змінних і констант, які в ньому знаходяться. Повинна бути можливість зміни значень цих об'єктів, а також можливість впливу на дерево виразу з метою його модифікацій. Об'єкт мат.вираження повинен мати метод розрахунку значення (шляхом обходу дерева мат.вирази), отримує в якості параметрів набір вхідних змінних і видає як результат чисельне значення.

Об'єкт мат.вираження повинен мати метод перетворення дерева мат.вирази в об'єкт System.Linq.Expression. І метод, що дозволяє отримати відразу скомпільований делегат на основі використання механізмів Linq.Expression.

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

Опис об'єктної моделі
Клас Парсера

Життя в об'єкті (після створення) починається з виклику методу Parse.

public MathExpression Parse(string StrExpression)
/// < summary>Розібрати рядок математичного виразу</summary>
/ / / < param name="StrExpression">Рядковий подання математичного виразу</param>
/ / / < returns>Математичне вираз</returns>
[NotNull]
public MathExpression Parse([NotNull] string StrExpression)
{
Contract.Requires(!string.IsNullOrWhiteSpace(StrExpression));
Contract.Ensures(Contract.Result<MathExpression>() != null);
StrPreprocessing(ref StrExpression);
OnStringPreprocessing(ref StrExpression);

var expression = new MathExpression(StrExpression, this);

ProcessVariables(expression);
ProcessFunctions(expression);

return expression;
}

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

Передобробка включає два етапи:

Приватний метод StrPreprocessing, видаляє зайві символи з рядка:

protected void StrPreprocessing(ref string Str)
/// < summary>Попередня обробка вхідної рядкового виразу</summary>
/ / / < param name="Str">Оброблювана рядок</param>
// Видалення з рядка всіх символів, з безлічі заборонених символів
protected virtual void StrPreprocessing([NotNull] ref string Str)
{
Contract.Requires(!string.IsNullOrEmpty(Str));
Contract.Ensures(!string.IsNullOrEmpty(Contract.ValueAtReturn(out Str)));
Str = new string Str.Where(f_ExcludeCharsSet.NotContains).ToArray());
}


і метод генерації події попередньої обробки рядка, що б користувач парсера міг самостійно підготувати рядок до аналізу:

public event EventHandler<EventArgs<string>> StringPreprocessing
/// < summary>Подія попередньої обробки вхідної рядка</summary>
public event EventHandler<EventArgs<string>> StringPreprocessing;

/// <summary>Генерація події попередньої обробки вхідної рядка</summary>
/ / / < param name="args">Аргумент події, що містить оброблювану рядок</param>
protected virtual void OnStringPreprocessing([NotNull] EventArgs<string> args)
{
Contract.Requires(args != null);
Contract.Requires(args.Argument != null);
Contract.Requires(args.Argument != string.Empty);
Contract.Ensures(args.Argument != null);
Contract.Ensures(args.Argument != string.Empty);
StringPreprocessing?.Invoke(this, args);
}

/// <summary>Генерація події попередньої обробки вхідної рядка</summary>
/ / / < param name="StrExpression">Оброблювана рядок</param>
private void OnStringPreprocessing([NotNull] ref string StrExpression)
{
Contract.Requires(!string.IsNullOrEmpty(StrExpression));
Contract.Ensures(Contract.ValueAtReturn(out StrExpression) != null);
Contract.Ensures(Contract.ValueAtReturn(out StrExpression) != string.Empty);
var args = new EventArgs<string>(StrExpression);
OnStringPreprocessing(args);
StrExpression = args.Argument;
}


Після того, як рядок очищена від сміття і підготовлена до розбору, вона передається в конструктор мат.вирази. Також у нього передається сам парсер, відповідальний за процеси визначення функцій, констант і змінних.

До класу парсера ще будемо повертатися в міру згадок його членів, а зараз… Клас Математичного виразу:

Діаграма

Конструктор, який передається виклик методу Parse:

internal MathExpression(string StrExpression, ExpressionParser Parser)
/// < summary>Ініціалізація нового математичного виразу</summary>
/ / / < param name="StrExpression">Рядковий подання виразу</param>
/ / / < param name="Parser">Посилання на парсер</param>
internal MathExpression([NotNull] string StrExpression, [NotNull] ExpressionParser Parser)
: this()
{
Contract.Requires(!string.IsNullOrEmpty(StrExpression));
Contract.Requires(Parser != null);
Contract.Ensures(Tree != null);
var terms = new BlockTerm(StrExpression); // розбити рядок на елементи
var root = terms.GetSubTree(Parser, this); // виділити корінь дерева з першого елемента
f_ExpressionTree = new ExpressionTree(root); // Створити дерево виразу з кореня
}


Знову ж таки, опускаючи блок контрактів, спочатку на основі рядка створюється ієрархічна об'єктна структура термів мат.вирази. Потім з самого першого блоку викликається метод отримання кореня дерева мат.вирази. І на його основі працює конструктор дерева.

На першому етапі розбору мат.вирази необхідно рядковий подання (послідовність символів) об'єднати в послідовність логічних блоків. Деякі з них можуть бути рекуррентно вкладені один в одного.

Ієрархія класів термів вираження

Рядок розбивається на 4 види термів:

  • BlockTerm — терм, що містить масив термів;
  • StringTerm — терм, що містить значення рядка
  • CharTerm — терм, що містить один символ, який не можна асоціювати з звичайними рядками;
  • NumberTerm — терм, що містить ціле число.
Таким чином, вся рядок спочатку представляється як один блоковий терм, всередині якого лежать складові елементи.

public BlockTerm(string Str)
/// < summary>Новий блок математичного виразу</summary>
/ / / < param name="Str">значення Рядка блоку</param>
public BlockTerm(string Str) : this("", Str, "") { }

/// <summary>Новий блок вираження</summary>
/ / / < param name="OpenBracket">Відкриваюча дужка</param>
/ / / < param name="Str">значення Рядка блоку</param>
/ / / < param name="CloseBracket">дужка Закривається</param>
public BlockTerm([NotNull] string OpenBracket, [NotNull] string Str, [NotNull] string CloseBracket)
: base(string.Format("{0}{2}{1}", OpenBracket ?? "", CloseBracket ?? "", Str))
{
Contract.Requires(!string.IsNullOrEmpty(Str));
f_OpenBracket = OpenBracket;
f_CloseBracket = CloseBracket;
f_Terms = GetTerms(Str);
}


Ну і базовий клас терма:

abstract class Term {...}
/// < summary>Елемент математичного виразу</summary>
abstract class Term
{
/// <summary>Рядковий вміст</summary>
protected string f_Value;

/// <summary>Конструктор елемента математичного виразу</summary>
/ / / < param name="Value">Рядковий вміст</param>
protected Term(string Value) { f_Value = Value; }

/// <summary>Метод вилучення піддерево для даного елемента математичного виразу</summary>
/ / / < param name="Parser">Парсер математичного виразу</param>
/ / / < param name="Expression">Математичне вираз</param>
/ / / < returns>Вузол дерева мат.вираження, що є поддеревом для даного елемента</returns>
[NotNull]
public abstract ExpressionTreeNode GetSubTree([NotNull] ExpressionParser Parser, [NotNull] MathExpression Expression);

/// <summary>Рядкове представлення елемента мат.вирази</summary>
/ / / < returns>Рядковий вміст елемента мат.вирази</returns>
public override string ToString() => f_Value;
}


Розбивка підрядка на складові елементи здійснюється методом GetTerms:

private static Term[] GetTerms(string Str)
/// < summary>Отримати список елементів математичного виразу з рядка</summary>
/ / / < param name="Str">Рядковий подання математичного виразу</param>
/ / / < returns>Масив елементів математичного виразу</returns>
[CanBeNull]
private static Term[] GetTerms([CanBeNull] string Str)
{
if(Str == null) return null;
if(Str.Length == 0) return new Term[0];
var pos = 0;
var len = Str.Length;
var result = new List<Term>();
while(pos < len)
{
var c = Str[pos];
if(char.IsLetter© || c == '∫')
{
Term value = new StringTerm(GetNameString(Str, ref pos));
if(pos < len)
switch(Str[pos])
{
case '(':
{
var blokStr = Str.GetBracketText(ref pos);
var block = new BlockTerm("(", blokStr, ")");
value = new FunctionTerm((StringTerm)value, block);
}
break;
case '[':
{
var blokStr = Str.GetBracketText(ref pos, "[", "]");
var block = new BlockTerm("[", blokStr, "]");
value = new FunctionTerm((StringTerm)value, block);
}
break;
case '{':
{
var blokStr = Str.GetBracketText(ref pos, "{", "}");
var block = new BlockTerm("{", blokStr, "}");
value = new FunctionTerm((StringTerm)value, block);
}
break;
}
if(pos < len && Str[pos] == '{')
value = new FunctionalTerm
(
(FunctionTerm)value,
new BlockTerm("{", Str.GetBracketText(ref pos, "{", "}"), "}")
);
result.Add(value);
}
else if(char.IsDigit©)
result.Add(new NumberTerm(GetNumberString(Str, ref pos)));
else
switch©
{
case '(':
{
var blokStr = Str.GetBracketText(ref pos);
var block = new BlockTerm("(", blokStr, ")");
result.Add(block);
}
break;
case '[':
{
var blokStr = Str.GetBracketText(ref pos, "[", "]");
var block = new BlockTerm("[", blokStr, "]");
result.Add(block);
}
break;
case '{':
{
var blokStr = Str.GetBracketText(ref pos, "{", "}");
var block = new BlockTerm("{", blokStr, "}");
result.Add(block);
}
break;
default:
result.Add(new CharTerm(Str[pos++]));
break;
}
}
return result.ToArray();
}


Метод починається з перевірок на порожнечу вхідний рядки і нульову довжину. Після фіксуються поточна позиція аналізованого символу в рядку і її довжина, після чого в циклі до досягнення кінця рядка символ розглядається в поточній позиції:

— Якщо він є буквою, або символом інтеграла, то йде спроба захоплення імені методом GetNameString.

private static string GetNameString(string Str, ref int pos)
/// < summary>Отримати ім'я з рядка</summary>
/ / / < param name="Str">Вихідна рядок</param>
/ / / < param name="pos">Положення у рядку</param>
/ / / < returns>Рядок імені</returns>
private static string GetNameString([NotNull] string Str, ref int pos)
{
Contract.Requires(!string.IsNullOrEmpty(Str));
Contract.Ensures(Contract.ValueAtReturn(out pos) >= 0);
Contract.Ensures(Contract.ValueAtReturn(out pos) < Str.Length);

var result = "";
var L = Str.Length;
var i = pos;
while(i < L && (char.IsLetter(Str[i]) || Str[i] == '∫'))
result += Str[i++];
if(і == L || !char.IsDigit(Str[i]))
{
pos = i;
return result;
}
while(i < L && char.IsDigit(Str[i]))
result += Str[i++];
pos += result.Length;
return result;
}


Після цього перевіряється поточний символ на предмет наявності в ньому дужки що відкривається. Якщо виявлена одна з дужок, то з рядка витягується вкладений блок, обмежений відкривається і відповідної їй закритою дужкою. Створений таким чином блоковий терм поміщається в конструктор функціонального терма із зазначенням встановленого раніше імені функції.

Підрядок, обмежена відкривається і закривається дужками виділяється з рядка методом-розширенням:

public static string GetBracketText(this string Str, ref int Offset, string Open, string Close)
/// < summary>
/// Виділення підрядка, обмеженою шаблоном початку і шаблоном закінчення рядка починаючи з вказаного зсуву
/ / / < /summary>
/ / / < param name="Str">Вхідний рядок</param>
/ / / < param name="Offset">
/// Зсув у вхідному рядку пошуку - в кінці роботи методу відповідає місцю закінчення пошуку
/// </param>
/ / / < param name="Open">Шаблон початку підрядка</param>
/ / / < param name="Close">Шаблон закінчення підрядка</param>
/ / / < returns>Підрядок, укладена між зазначеними шаблонами початку і закінчення</returns>
/// <exception cref="FormatException">
/// Якщо шаблон завершення рядка на нейден, або якщо кількість шаблонів початку рядка перевищує 
/// кількість шаблонів закінчення у вхідному рядку
/// </exception>
public static string GetBracketText(this string Str, ref int Offset, string Open = "(", string Close = ")")
{
var Start = Str.IndexOf(Open, Offset, StringComparison.Ordinal);
if(Start == -1) return null;
var Stop = Str.IndexOf(Close, Start + 1, StringComparison.Ordinal);
if(Stop == -1)
throw new FormatException();
var start = Start;
do
{
start = Str.IndexOf(Open, start + 1, StringComparison.Ordinal);
if(start != -1 && start < Stop)
Stop = Str.IndexOf(Close, Stop + 1, StringComparison.Ordinal);
} while(start != -1 && start < Stop);
if(Stop == -1 || Stop < Start)
throw new FormatException();
Offset = Stop + Close.Length;
Start += Open.Length;
return Str.Substring(Start, Stop - Start);
}


Спочатку визначаються індекси перших входжень символів початку і кінця. Якщо початковий символ не знайдений, то повертаємо порожнечу. Якщо на знайдений кінцевий символ, то це помилка формату.

Ідея методу полягає в послідовному циклічному пошуку шаблонів початку і закінчення рядка. Намагаємося знайти відкриває наступний символ. Якщо він знайдений, і він стоїть до закриваючого, то треба оновити індекс закриває символу. Цикл продовжується до тих пір, поки не буде такої кінцевий символ, якому не передує відкриває.

В результат методу потрапляє підрядок, розташована між відкриваючим і закриваючим символом.

Якщо після сформованого функціонального терма знайдена відкривається фігурна дужка, то це починається тіло функціоналу. Виділяємо вміст фігурних дужок у блок і створює терм-функціонал, вказуючи йому терм-функцію, яка буде в цьому контексті містити ім'я функціоналу і його параметри, а тілом блок у фігурних дужках.

Якщо дужок виявлено не було, то знайдене ім'я є литералом (майбутньої змінної… або константою).

— Якщо черговий символ рядка був цифрою, то починається ціле число. Виділяємо підрядок, який містить лише цифри.

private static string GetNumberString(string Str, ref int pos)
/// < summary>Отримати цифрову рядок</summary>
/ / / < param name="Str">Досліджувана рядок</param>
/ / / < param name="pos">Вихідна позиція у рядку</param>
/ / / < returns>Рядок цифрового значення</returns>
private static string GetNumberString([NotNull] string Str, ref int pos)
{
Contract.Requires(!string.IsNullOrEmpty(Str));
Contract.Ensures(Contract.ValueAtReturn(out pos) >= 0);
Contract.Ensures(Contract.ValueAtReturn(out pos) < Str.Length);

var p = pos;
var l = Str.Length;
while(p < l && !char.IsDigit(Str, p)) p++;
if(p >= l) return null;
var start = p;
while(p < l && char.IsDigit(Str, p)) p++;
pos = p;
return Str.Substring(start, p - start);
}


Результат роботи цього методу — рядок з цифрами — потрапляє в конструктор цілочисельного терма.

— Якщо черговий символ рядка — це відкриваюча дужка, то починається блок. Виділяємо його підрядок методом-розширенням GetBracketText.
— Якщо черговий символ — не дужка, то це невизначений символ, який перетворюється в символьний терм.

Всі створені терми спершу збираються з списку, а потім повертаються у вигляді масиву.

Конструктори всіх інших термів менш цікаві. Вони просто зберігають отримані параметри у внутрішніх полях (можливо, з перетворенням типів).

Після цього рядок буде перетворена в логічну ієрархічну структуру вкладених один в одного послідовностей термів різного виду. З цієї послідовності рекуррентно будується бінарне дерево мат.вирази.

Основою дерева є базовий клас вузла дерева мат.вирази.

Ієрархія класів

Кожен вузол — клас, який зберігає посилання на сайт-корінь лівого піддерева і вузол-корінь правого піддерева, а також посилання на свого предка. Абстрактний клас вузла дерева надає інтерфейс для доступу до вузлів предкам/нащадкам, методи обходу, функціональні індексатори, що дозволяють отримати перерахування вузлів, пов'язаних з поточним, рекурсивні методи одержання відомих даного вузла (як кореня свого піддерева) змінних, функцій і т. п. Також базовий клас вузла надає ряд вычислимых властивостей: ознаки — є вузол лівим/правим поддеревом, є вузол коренем, посилання на корінь дерева, символьний шлях до поточного вузла з кореня дерева і ітератор предків.

Вузол дерева

Код цього класу дозволяє здійснювати основні маніпуляції з елементами дерева для їх заміни, перестановки, обходу доступу. Окремі методи я наведу у міру потреби. Повний текст займе багато місця. Його можна подивитися/скопіювати з вихідного коду проекту.

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

Вузли розбору включають в себе рядковий вузол, символьний вузол і вузол інтервального значення. Вони потрібні для доповнення певних метоструктур в дереві начебто функціоналу інтегрування, де треба вказати інтервал.

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

У їх число входить:

  • ComputedBracketNode — представляє блок дужок
  • ValueNode — абстрактний клас, що представляє вузол-значення. Його спадкоємці:
    — ConstValueNode — вузол числового значення
    — VariableValueNode — вузол змінної (яка може бути і константою начебто pi, e, ...)

  • FunctionNode — сайт, що представляє об'єкт-функцію
  • FunctionalNode — сайт, що представляє об'єкт-функціонал
  • ОператорNode — абстрактний клас вузла-оператора. Його спадкоємці
    — Найпростіші мат.оператори +,-,*,/,^;
    — Логічні оператори"==, !, ><, &&, ||;
    — Оператор умови"<результат_работы_логического оператора>?" і спільно з ним використовується оператор вибору "<вариант_1>:<вариант_2>"
    — Оператор, що реалізує доступ до аргументу функції і використовується спільно з ним оператор доступу до імені аргументу функції.

Процес побудови дерева
Клас терма оголошує абстрактний метод GetSubTree, який дозволяє з будь-якого терма отримати описуване їм піддерево. Процес побудови дерева починається з виклику цього методу у сформованого з вихідної рядки блокового терма.

public override ExpressionTreeNode GetSubTree(ExpressionParser Parser, MathExpression Expression)
/// < summary>Отримати корінь піддерева виразів</summary>
/ / / < param name="Parser">Парсер вираження</param>
/ / / < param name="Expression">Математичне вираз</param>
/ / / < returns>Корінь піддерева</returns>
public override ExpressionTreeNode GetSubTree(ExpressionParser Parser, MathExpression Expression)
{
Contract.Requires(Parser != null);
Contract.Requires(Expression != null);
Contract.Ensures(Contract.Result<ExpressionTreeNode>() != null);
var separator = Parser.ExpressionSeparator; // фіксуємо символ-роздільник виразів
// Розбиваємо послідовність елементів вирази на групи, розділені симовлом-роздільником
// Витягуємо з кожної групи корінь дерева виразів і складаємо їх в масив
var roots = Terms
.Split(t => t is CharTerm && ((CharTerm)t).Value == separator)
.Select(g => Parser.GetRoot(g, Expression)).ToArray();


if(roots.Length == 1) return roots[0]; // Якщо знайдений тільки один корінь, то повертаємо його
// Інакше коренів знайдено багато
ExpressionTreeNode argument = null; // оголошуємо посилання на аргумент
for(var i = 0; i < roots.Length; i++) // проходимо по всіх знайдених коренів
{
var root = roots[i];
ExpressionTreeNode arg; // Якщо черговий корінь дерева
if(root is FunctionArgumentNode) // - аргумент функції
arg = "root"; / / -- залишаємо його без змін
else if(root is FunctionArgumentNameNode) // - вузол імені аргументу
// -- створюємо новий іменований аргумент функції
arg = new FunctionArgumentNode(root as FunctionArgumentNameNode);
else if(root is VariantOperatorNode && root.Left is VariableValueNode)
arg = new FunctionArgumentNode(((VariableValueNode)root.Left).Name, root.Right);
else // - у всіх інших випадках
arg = new FunctionArgumentNode("", root); // -- створюємо аргумент функції без імені
if(argument == null) argument = arg; // Якщо аргумент не вказано, то зберігаємо отриманий вузол, як аргумент
else // інакше
argument = argument.Right = arg; // зберігаємо отриманий вузол в праве піддерево аргументу
}
// Якщо аргумент не був виділений, то щось пішло не так - помилка формату
if(argument == null) throw new FormatException("Не визначено аргумент функції");
return argument.Root; // Повернути корінь аргументу
}


Метод витягує з переданого йому об'єкта мат.вирази символ, який розділяє вираження в блоці. За замовчуванням встановлений роздільник ';' — крапка з комою.

Потім в Linq-послідовності весь масив вкладених термів розбивається на подмассивы за роздільником — символьним терму, що містить символ-роздільник виразів. За це відповідає метод-розширення Split.

public static T[][] Split<T>(this T[] array, Func<T, bool> Splitter)
/// < summary>Розділити вхідний масив на подмассивы зазначеним методом</summary>
/// <typeparam name="T">Тип елементів масиву</typeparam>
/ / / < param name="array">Розділяється масив</param>
/ / / < param name="Splitter">Метод, що повертає істину, коли треба почати новий подмассив</param>
/ / / < returns>
/// Масив подмассивов елементів вихідного масиву, розділений обраними зазначеним методом елементами.
/// Вибрані елементи у результат не входять.
/// </returns>
[NotNull]
public static T[][] Split<T>([NotNull] this T[] array, [NotNull] Func<T, bool> Splitter)
{
Contract.Requires(array != null);
Contract.Requires(Splitter != null);
Contract.Ensures(Contract.Result<T[][]>() != null);

var result = new List<T[]>(array.Length);
var aggregator = new List<T>(array.Length);

for(var i = 0; i < array.Length; i++)
{
var value = array[i];
if(Splitter(value) && aggregator.Count != 0)
{
result.Add(aggregator.ToArray());
aggregator.Clear();
}
else
aggregator.Add(value);
}
if(aggregator.Count != 0)
result.Add(aggregator.ToArray());

return result.ToArray();
}


Для кожного з подмассива термів викликається метод парсера GetRoot, який покликаний визначити корінь дерева цієї групи термів. Потім всі знайдені коріння об'єднуються в масив.

Метод GetRoot:

internal ExpressionTreeNode GetRoot(Term[] Group, MathExpression MathExpression)
/// < summary>Метод добування кореня дерева з послідовності елементів математичного виразу</summary>
/ / / < param name="Group">група елементів математичного виразу</param>
/ / / < param name="MathExpression">Посилання на математичний вираз</param>
/ / / < returns>Корінь дерева мат.вирази</returns>
internal ExpressionTreeNode GetRoot([NotNull] Term[] Group, [NotNull] MathExpression MathExpression)
{
Contract.Requires(Group != null);
Contract.Requires(MathExpression != null);
Contract.Ensures(Contract.Result<ExpressionTreeNode>() != null);

// Посилання на останній оброблений вузол дерева
ExpressionTreeNode Last = null;
for(var i = 0; i < Group.Length; i++) // цикл по всім елементам групи
{
var node = Group[i].GetSubTree(this, MathExpression); // отримати піддерево для поточного елемента групи
// Якщо черговий елемент групи...
if(Group[i] is NumberTerm) // ...черговий елемент число, то
{
//...якщо є попереду ще два елемента і пройшла вдала спроба вважати роздільник дробового числи і мантиссу 
if(i + 2 < Group.Length && NumberTerm.TryAddFractionPart(ref node, Group[i + 1], DecimalSeparator, Group[i + 2]))
i += 2; //...то збільшити індекс поточної групи на два.
}
else if(Group[i] is BlockTerm) //...черговий елемент блок (з дужками)
node = new ComputedBracketNode( // черговий вузол дерева - це вычислимый блок
new Bracket( //вид дужок:
(((BlockTerm)Group[i]).OpenBracket), // копіюємо вид відкриваючої дужки
((BlockTerm)Group[i]).CloseBracket), // копіюємо вид закриваючої дужки
node); //Всередині вузол дерева

//Проводимо комбінацію поточного вузла попереднім вузлом дерева
Combine(Last, Last = node); // і призначаємо поточний вузол дерева попереднім

if(Last.IsRoot && Last is VariantOperatorNode && Last.Left is VariableValueNode)
Last = new FunctionArgumentNameNode(((VariableValueNode)Last.Left).Name);

OnNewNodeAdded(ref Last);
}

// Якщо посилання на попередній вузол відсутній, то це помилка формату
if(Last == null) throw new FormatException();
return Last.Root; // повернути корінь дерева поточного елемента
}


Тут послідовно проглядаються вхідний масив термів вираження. З кожного чергового терма вираження ми витягаємо корінь дерева (тут виникає рекурсія). Після чого треба перевірити:

— якщо поточний терм цілочисельний і він як мінімум третій з кінця масиву, то намагаємося додати до поточного вузла дробову частину.

public static bool TryAddFractionPart(ref ExpressionTreeNode node, Term SeparatorTerm, char DecimalSeparator, Term FrationPartTerm)
/// < summary>Спробувати додати дробове значення числа</summary>
/ / / < param name="node">Вузол вираження</param>
/ / / < param name="SeparatorTerm">Блок роздільник</param>
/ / / < param name="DecimalSeparator">Блок з цілою частиною числа</param>
/ / / < param name="FrationPartTerm">Блок з дробовою частиною числа</param>
/ / / < returns>Істина, якщо дія здійснено успішно. Брехня, якщо у наступних блоках не містить потрібної інформації</returns>
public static bool TryAddFractionPart(ref ExpressionTreeNode node, Term SeparatorTerm, char DecimalSeparator, Term FrationPartTerm)
{
var value = node as ConstValueNode;
if(value == null) throw new ArgumentException("Невірний тип вузла дерева");
var separator = SeparatorTerm as CharTerm;
if(separator == null || separator.Value != DecimalSeparator) return false;
var fraction = FrationPartTerm as NumberTerm;
if(fraction == null) return false;

var v_value = fraction.Value;
if(v_value == 0) return true;
node = new ConstValueNode(value.Value + v_value / Math.Pow(10, Math.Truncate(Math.Log10(v_value)) + 1));
return true;
}


Методом вказується символ-роздільник цілої і дробової частини десяткового числа, а також два наступні за поточним терма. Якщо другий терм — символьний і містить символ-роздільник, а третій числовий, то вузол замінюється на новий сайт-константне значення
— Друга перевірка, якщо поточний терм блочний, то формується вузол-блок_скобок.

По завершенні перевірок виконується метод, що комбінує вузол, створений на попередньому циклі з поточним:

public virtual void Combine(ExpressionTreeNode Last, ExpressionTreeNode Node)
/// < summary>Комбінація попереднього і поточного вузлів дерева</summary>
/ / / < param name="Last">Попередній вузол дерева (вже інтегрований у дерево)</param>
/ / / < param name="Node">Поточний вузол, який треба вставити в дерево</param>
// ReSharper disable once CyclomaticComplexity
public virtual void Combine([CanBeNull] ExpressionTreeNode Last, [NotNull] ExpressionTreeNode Node)
{
Contract.Requires(Node != null);
if(Last == null) return; // Якщо попередній вузол дерева не вказаний, повернення

if(Node is CharNode) // Якщо поточний вузол - символьний вузол, то
{
Last.LastRightChild = Node; // просто призначити його самим правим дочірнім
return;
}

var operator_node = Node as OperatorNode; // представляємо поточний вузол у вигляді вузла-оператора
if(operator_node != null) // якщо поточний вузол є оператором...
{
//Намагаємося отримати оператор попереднього вузла:
// намагаємося привести попередній вузол до типу оператора вузла
// або намагаємося привести батьківський вузол попереднього вузла до типу оператора вузла
var parent_operator = as Last OperatorNode ?? Last.Parent as OperatorNode;

if(parent_operator != null) // Якщо отримана посилання не попередній вузол-оператор (і поточний є оператором)... 
{
// Якщо ліве піддерево попереднього оператор порожньо і батько попереднього оператора - теж оператор
// op <- встановлюємо попереднім оператором батьків
// |
// op 
// / \
// null ?
if(parent_operator.Left == null && parent_operator.Parent is OperatorNode)
parent_operator = (OperatorNode)parent_operator.Parent;


if(parent_operator.Left == null) // Якщо ліве піддерево попереднього оператора порожньо...
operator_node.Left = parent_operator; // встановлюємо попередній оператор в якості лівого піддерева поточного
else if(parent_operator.Right == null) // Інакше якщо праве піддерево порожньо
parent_operator.Right = Node; // встановити поточний оператор правим поддеревом попереднього
else // Інакше якщо конфлікт пріоритетів
{
var priority = operator_node.Priority; // отримуємо пріоритет поточного вузла
// Якщо пріоритет поточного оператора менше, або дорівнює пріоритету попереднього
if(priority <= parent_operator.Priority)
{
// то треба підніматися вгору під дереву до тих пір
parent_operator = (OperatorNode)parent_operator.Parents
// поки що зустрічаються на шляху оператори мають пріоритет вище пріоритету поточного оператора
.TakeWhile(n => n is OperatorNode && priority <= ((OperatorNode)n).Priority)
// взяти останню з послідовності
.LastOrDefault() ?? parent_operator; // якщо повернулася порожня посилання, то взяти попередній оператор

// На поточний момент попередній оператор має пріоритет вище пріоритету поточного оператора

if(parent_operator.IsRoot) // Якщо попередній оператор - корінь дерева
// Якщо пріоритет оператора в корені дерева більше, або дорівнює пріоритету поточного оператора
if(priority <= parent_operator.Priority)
// Присвоїти лівому поддереву поточного оператора попередній
operator_node.Left = parent_operator;
else // Інакше якщо попередній оператор не корінь
{
var parent = parent_operator.Parent; // зберегти на батька попереднього оператора
parent.Right = Node; // записати поточний оператор в якості правого піддерева
operator_node.Left = parent_operator;// записати попередній оператора лівим поддеревом поточного
}
}
else //якщо пріоритет поточного оператора більше пріоритету попереднього
{
// то треба спускатися в праве піддерево до тих пір
parent_operator = (OperatorNode)parent_operator.RightNodes
// поки що зустрічаються на шляху оператори мають ліві піддерева і пріоритет операторів менше поточного
.TakeWhile(n => n is OperatorNode && n.Left != null && ((OperatorNode)n).Priority < priority)
// взяти останню з послідовності
.LastOrDefault() ?? parent_operator; // якщо повернулася порожня посилання, то взяти попередній оператор

// На поточний момент попередній оператор має пріоритет нижче пріоритету поточного оператора

var right = parent_operator.Right; // зберегти праве піддерево попереднього оператора
parent_operator.Right = Node; // в праве піддерево попереднього оператора потрапляє поточний
operator_node.Left = right; // в ліве піддерево поточного оператора записується збережене праве 
}
}
}
else // Якщо попередній вузол не є оператором
{
var parent = Last.Parent;
var is_left = Last.IsLeftSubtree;
var is_right = Last.IsRightSubtree;
operator_node.Left = Last; // записати попередній вузол лівим поддеревом поточного
if(is_left)
parent.Left = operator_node;
else if(is_right)
parent.Right = operator_node;
}
return; // повернення
}
// Якщо поточний вузол не є оператором

if(Last is OperatorNode) // якщо попередній вузол є оператором
{
Last.Right = Node; // додати поточний в праве піддерево попереднього
return; // повернення
}
// Якщо ні поточний не попередній вузли не є операторами

//Якщо попередній вузол був числом, або попередній вузол був дужками і поточний - дужки
if(Last is ConstValueNode || (Last is ComputedBracketNode && Node is ComputedBracketNode))
{
//Зберігаємо посилання на батька попереднього вузла
var parent = Last.Parent;
if(parent != null) // якщо батько є
// в праве піддерево батьків записуємо оператор множення попереднього і поточного вузла
parent.Right = new MultiplicationOperatorNode(Last, Node);
else // якщо попередній вузол був вузлом дерева
// створюємо новий вузол-оператор множення попереднього вузла і поточного, який стає новим коренем дерева
new MultiplicationOperatorNode(Last, Node);
return; // повернення.
}

Last.Right = Node;
}


Це один з центральних методів. Він, у відповідності з логікою створюваного дерева мат.вирази, приєднує нові вузли до вже існуючого, враховуючи вузли-оператори (їх пріоритети). Безумовно, метод вимагає рефакторінгу з причини розміру обсягів коду в ньому. Логіка його роботи відображено в коментарях в коді.

Коли комбінація двох вузлів виконана проводиться остання перевірка: якщо оброблений вузол — корінь дерева і вузол є вузлом варіантів вибору, і при цьому в його лівому поддереве вузол-мінлива <Змінна>:<вариант_2>, то його слід розглядати як вузол аргументу функції <имя_аргумента>:<значение_аргумента>. При цьому ім'ям аргументу стає ім'я змінної.

По завершенні ітерації в об'єкті-парсере генерується подія NewNodeAdded, яке передається створений сайт для зовнішньої обробки його користувачем. При цьому вузол передається по посиланню, так що існує можливість повністю змінити створюване дерево.

Після того, як для групи термів парсером було створено піддерево і в методі GetSubTree блокового терма всі корені таких піддерев були об'єднані в масив, метод перевіряє:

  • якщо масив містить лише один елемент, то він і повертається в якості результату (це тривіальний випадок)
  • якщо коренів багато, то кожен з них упаковується в вузол аргументу функції FunctionArgumentNode ( в його праве піддерево), а в ліве піддерево чіпляється наступний аргумент.
Структура дерева матвыражения
Таким чином формується дерево відповідає наступним правилам:

  • Вузли операторів містять свої операнди в лівому і правому поддеревьях
  • Вузли значень змінних, числові значення) повинні бути листям дерева (але не обов'язково)
  • Вузли блоків дужок зберігають значення в лівому поддереве
  • Вузли функцій мають пусте ліве піддерево. У правому поддереве зберігається вузол першого аргументу.
  • Вузол аргументу функції в лівому поддереве зберігається або корінь дерева аргументу, або вузол імені аргументу. У правому поддереве зберігається посилання на наступний аргумент функції;
  • Вузол імені аргументу — в лівому поддереве зберігається невычислимый рядковий вузол з рядком-ім'ям, а правом поддереве зберігається корінь дерева-аргументу.
  • Вузол функціоналу є листом. Параметри і вираз зберігаються окремо (що дозволяє більш гнучко змінити логіку роботи з ними).
Самі функції і функціонали виділені в окремі об'єкти.

Дерево побудовано. У процесі побудови були створені у тому числі вузли змінних і функцій. Кожен такий сайт був створений відповідним йому термо прямим викликом конструктора сайту відповідного типу:

class StringTerm: Term {...}
/// < summary>Рядковий елемент вираження</summary>
class StringTerm : Term
{
/// <summary>Ім'я строкового елемента</summary>
[NotNull]
public string Name => f_Value;

/// <summary>Новий рядковий елемент</summary>
/ / / < param name="Name">Ім'я строкового елемента</param>
public StringTerm([NotNull] string Name) : base(Name) { Contract.Requires(!string.IsNullOrEmpty(Name)); }

/// <summary>Піддерево елемента, що складається з вузла-змінної</summary>
/ / / < param name="Parser">Парсер</param>
/ / / < param name="Expression">Математичне вираз</param>
/ / / < returns>Вузол дерева з змінної, отриманої з Expression.Variable[Name]</returns>
public override ExpressionTreeNode GetSubTree(ExpressionParser Parser, MathExpression Expression)
=> new VariableValueNode(Expression.Variable[Name]);
}

/// <summary>Блок визначення функції</summary>
sealed class FunctionalTerm : FunctionTerm
{
/// <summary>Параметри оператора</summary>
[NotNull]
public BlockTerm Parameters { get; set; }

/// <summary>Ініціалізація блоку комплексного оператора</summary>
/ / / < param name="Header">Заголовок блоку</param>
/ / / < param name="Body">Тіло блоку</param>
public FunctionalTerm([NotNull] FunctionTerm Header, [NotNull] BlockTerm Body) : base(Header.Name, Body)
{
Contract.Requires(Header != null);
Contract.Requires(Body != null);
Parameters = Header.Block;
}

/// <summary>Отримати піддерево комплексного оператора</summary>
/ / / < param name="Parser">Парсер</param>
/ / / < param name="Expression">Математичне вираз</param>
/ / / < returns>Вузол комплексного оператора</returns>
public override ExpressionTreeNode GetSubTree(ExpressionParser Parser, MathExpression Expression)
=> new FunctionalNode(this, Parser, Expression);

public override string ToString() => $"{Name}{Parameters}{Block}";
}


При цьому, при створенні сайт вимагає від вираження колекцію змінних/функцій що б витягти з неї по імені потрібний об'єкт-змінну/функцію.

internal FunctionNode(FunctionTerm Term, ExpressionParser Parser, MathExpression Expression)
/// < summary>Ініціалізація нового функціонального вузла</summary>
/ / / < param name="Term">Вираз функції</param>
/ / / < param name="Parser">Парсер вираження</param>
/ / / < param name="Expression">Математичне вираз</param>
internal FunctionNode(FunctionTerm Term, ExpressionParser Parser, MathExpression Expression)
: this(Term.Name)
{
var arg = Term.Block.GetSubTree(Parser, Expression);
if(!(arg is FunctionArgumentNode))
if(arg is FunctionArgumentNameNode)
arg = new FunctionArgumentNode((FunctionArgumentNameNode)arg);
else if(arg is VariableValueNode)
arg = new FunctionArgumentNode null, arg);
else if(arg is VariantOperatorNode && arg.Left is VariableValueNode)
arg = new FunctionArgumentNode(((VariableValueNode)arg.Left).Name, arg.Right);
else
arg = new FunctionArgumentNode null, arg);
Right = arg;
//Визначення об'єкту-функції з виразу
Function = Expression.Functions[Name, ArgumentsNames];
}


Після створення дерева в об'єкті мат.вирази колекції змінних і функцій будуть містити перелік використаних об'єктів. Але їх значення будуть порожні. Частина змінних необхідно класифікувати, як відомі парсеру константи (перенести їх на відповідну колекцію, об'єктам-функцій треба визначити реалізують їх делегати.

Ініціалізація дерева
Після створення «сирого» дерева мат.вираження його треба заповнити значеннями змінних і функцій. Метод Parse парсера послідовно викликає два методу ProcessVariables і ProcessFunctions передаючи в них створене сире дерево.

Метод обробки змінних:

internal void ProcessVariables(MathExpression Expression)
/// < summary>Обробка змінних</summary>
/ / / < param name="Expression">Оброблюваний математичне вираз</param>
internal void ProcessVariables([NotNull] MathExpression Expression)
{
Contract.Requires(Expression != null);
var tree_vars = Expression.Tree.Root.GetVariables().ToArray();
Expression.Variable
.Where(v => !tree_vars.Contains(v))
.ToArray()
.Foreach(v => Expression.Variable.Remove(v));
foreach(var variable in Expression.Variable.ToArray())
{
if(f_Constans.ContainsKey(variable.Name))
{
Expression.Variable.MoveToConstCollection(variable);
variable.Value = f_Constans[variable.Name];
}
OnVariableProcessing(variable);
}
}


Його завдання — обійти дерево, знайти всі вузли-змінні і витягти з них використані в них об'єкти-змінні. Після чого треба з колекції змінних мат.вирази видалити все, що не використовується в дереві.

Після цього для кожної змінної в дереві перевіряється, не містить її ім'я в колекції відомих констант парсера. Якщо так, то вона вилучається з колекції змінних мат.вирази, заноситься в колекцію констант мат.вирази, ініціалізується значенням, відомим парсеру і в ній виставляється прапор, що вона є константою.

Після цього для неї в парсере викликається подія виявлення нової змінної. При обробці цієї події користувач програми може перевизначити значення цієї змінної, або змінити сам об'єкт змінної.

Другий метод ProcessFunctions заповнює делегатами відомі мат.висловом функції:

internal void ProcessFunctions(MathExpression Expression)
/// < summary>Обробка функцій</summary>
/ / / < param name="Expression">Оброблюваний математичне вираз</param>
[SuppressMessage("ReSharper", "CyclomaticComplexity")]
internal void ProcessFunctions([NotNull] MathExpression Expression)
{
Contract.Requires(Expression != null);
foreach(var function in Expression.Functions)
switch(function.Name)
{
case "Sin":
case "SIN":
case "sin":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(Math.Sin);
break;
case "COS":
case "Cos":
case "cos":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(Math.Cos);
break;
case "TAN":
case "Tan":
case "tan":
case "tn":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(Math.Tan);
break;
case "ATAN":
case "ATan":
case "Atan":
case "atan":
case "atn":
case "Atn":
if(function.Arguments.Length == 1)
function.Delegate = new Func<double, double>(Math.Atan);
else if(function.Arguments.Length == 2)
function.Delegate = new Func<double, double, double>(Math.Atan2);
else goto default;
break;
case "Atan2":
case "atan2":
if(function.Arguments.Length != 2)
goto default;
function.Delegate = new Func<double, double, double>(Math.Atan2);
break;
case "CTG":
case "Ctg":
case "ctg":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(x => 1 / Math.Tan(x));
break;
case "Sign":
case "sign":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(x => Math.Sign(x));
break;
case "Abs":
case "abs":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(Math.Abs);
break;
case "Exp":
case "EXP":
case "exp":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(Math.Exp);
break;
case "Sqrt":
case "SQRT":
case "√":
case "sqrt":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(Math.Sqrt);
break;
case "log10":
case "Log10":
case "LOG10":
case "lg":
case "Lg":
case "LG":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(Math.Log10);
break;
case "loge":
case "Loge":
case "LOGe":
case "ln":
case "Ln":
case "LN":
if(function.Arguments.Length != 1)
goto default;
function.Delegate = new Func<double, double>(Math.Log);
break;
case "log":
case "Log":
case "LOG":
if(function.Arguments.Length != 2)
goto default;
function.Delegate = new Func<double, double, double>(Math.Log);
break;
default:
var f = OnFunctionFind(function.Name, function.Arguments);
if(f == null)
throw new NotSupportedException($"Обробка функції {function.Name} не підтримується");
function.Delegate = f;
break;
}
}


Якщо ім'я функції знаходиться серед варіантів оператора case, то при збігу необхідного функції числа аргументів їй призначається делегат, який буде обчислювати її значення. Якщо функція не визначена, то делегат визначається, як результат генерації події виявлення невідомої функції. При цьому користувач може визначити в реакції на цю подію необхідна по імені і кількістю (і іменах) аргументів необхідний делегат.

На цьому генерація мат.вирази завершується.

Використання
Припустимо, що у нас стоїть завдання обчислити інтеграл функції A*cos(2*x)/pi+G(x/2) деленый на А і + 1, де G(x)=2cos(x). При А, скажімо, дорівнює 5. І інтеграл треба взяти з кроком 0,05.

var parser = new ExpressionParser();
parser .FindFunction += (s, e) =>
{
if(e.SignatureEqual(name: "G", ArgumentsCount: 1))
e.Function = new Func<double, double>(x => 2 * Math.Cos(x));
};
var expr = parser.Parse(@"Int[x=-10..10;dx=0.05]{A*cos(2x) + G(x/2)}/A + 1");
expr.Variable["A"] = 5;
var y = expr.Compute(); //y = 0.30928806858920344 
var f = expr.Compile();
var y2 = f(); //y = 0.30928806858920344

Висновок
Враховуючи отриманий обсяг статті я ставлю тут… не крапку, а крапку з комою. В результаті вищевикладеного вдалося:

  1. Отримати загальне уявлення про метод вирішення поставленого завдання;
  2. Сформувати об'єктну модель парсера мат.виразів, самого мат.вирази і його дерева;
  3. Створити ефективний метод розбору рядка мат.вирази на логічні складові;
  4. Створити ефективний метод побудови дерева мат.вирази з урахуванням особливостей використання дужок, пріоритетів операторів і спеціальних конструкцій (функціоналів);
  5. Забезпечити контроль над різними етапами обробки вхідних даних парсером на основі системи подій;
  6. Додати можливість розширення функціональності.
Чого не вдалося описати в цій статті:

  1. Логіку роботи змінних (їх типи та методи їх отримання і подальшої заміни);
  2. Структуру колекцій змінних, констант, функцій, що беруть участь у роботі мат.вирази;
  3. Метод розрахунку значення мат.вираження шляхом обходу його дерева;
  4. Метод компіляції дерева мат.вирази делегат.
Чого не вдалося поки в реалізації самого парсера:

  1. Реалізувати методи оптимізації дерева мат.вирази;
  2. Прибрати милиці з ряду місць;
  3. Додати перевірки на предмет відповідності вхідних даних формату мат.вирази;
  4. Власне, окреслити межі цього формату;
  5. Підвищити покриття коду модульними тестами;
  6. Провести порівняльні дослідження швидкодії як етапів розбору, так і етапів обчислення.
В цілому, робота над цим кодом на жаль носить фоновий характер і триває вже кілька років, але на даному етапі він вже вирішує поставлені перед ним завдання. Хоча в теперішньому вигляді пускати його в продакшен не можна.

Повні вихідні коди можна знайти тут.

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.