Для учёбы необходимо было написать парсер арифметических операций, который мог бы рассчитывать не только простейшие операции, но и работать со скобками и функциями.
В интернете не нашел готовых и подходящих для меня решений (некоторые были чересчур сложные, другие были не полностью удовлетворяли условиям моей задачи). Немного погрустив, приступил к решению задачи самостоятельно и теперь хочу поделиться своимг****кодом оригинальным решением с миром.
Первая проблема, с которой я столкнулся — скобки. Мало того, что они должны выполняться первыми, так внутри них также могут находиться скобки. И так далее.

Точно такая же история с функциями — в параметрах функции могут находится другие функции и даже целые выражения.

Но об этом чуть-чуть позже. Для начала надо спарсить все выражение. Заметим, что у нас могут встретиться или скобка, или число, или операнд(+, -, *, /, ^), или функция, или константа.
Создадим для всего этого дела списки:
И будем проверять по очереди каждый символ. Естественно, если мы встретили знак "+" или "-" не после цифры, значит этот знак обозначает положительность или отрицательность числа, соответственно.
В примере проскакивала функция GetParametrs. Она нужна для случаев, когда функция имеет 2 параметра. Дело в том, что нельзя сделать простой Split. У нас может быть такое выражение:

Итак, выражение разбито на более мелкие, и кроме того, значения функций уже вычислены и подставлены в основное выражение как числа.
Скобки можно обработать по тому же принципу — сразу рассчитывать их и подставлять их как числа:
С нахождением индекса закрывающейся скобки опять все немного сложнее. Нельзя просто взять следующую закрывающуюся скобку. Это не с��аботает для вложенных скобок.
Основная часть программы написана. Осталось реализовать расчет обычных арифметических выражений. Чтобы не париться с порядком выполнения действий, я решил записывать выражение в польской нотации:
Ну и наконец, стеком рассчитываем значение выражения:
Если всё прошло хорошо, в result[0] будет лежать результат.
→ Ссылка на GitHub с полным кодом
В интернете не нашел готовых и подходящих для меня решений (некоторые были чересчур сложные, другие были не полностью удовлетворяли условиям моей задачи). Немного погрустив, приступил к решению задачи самостоятельно и теперь хочу поделиться своим
Первая проблема, с которой я столкнулся — скобки. Мало того, что они должны выполняться первыми, так внутри них также могут находиться скобки. И так далее.
Точно такая же история с функциями — в параметрах функции могут находится другие функции и даже целые выражения.
Но об этом чуть-чуть позже. Для начала надо спарсить все выражение. Заметим, что у нас могут встретиться или скобка, или число, или операнд(+, -, *, /, ^), или функция, или константа.
Создадим для всего этого дела списки:
public static List<string> digits = new List<string>() { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "," };
public static List<string> operands = new List<string>() {"^", "/", "*", "+", "-"};
public static List<string> functions = new List<string>() { "sqrt", "sin", "cos", "log", "abs"};
public static List<string> brackets = new List<string>() { "(", ")" };
public static List<string> constants = new List<string>() { "pi" };
public static Dictionary<string, string> constantsValues = new Dictionary<string, string>() { ["pi"] = "3,14159265359" };
И будем проверять по очереди каждый символ. Естественно, если мы встретили знак "+" или "-" не после цифры, значит этот знак обозначает положительность или отрицательность числа, соответственно.
for (int i = 0; i < expression.Length; i++) {
if (brackets.Contains(expression[i].ToString())){
if (lastSymbol != ""){
symbols.Add(lastSymbol);
lastSymbol = "";
} // на случай, если до скобки шло число
symbols.Add(expression[i].ToString()); // Если встретили скобку - без разбирательств добавляем ее в массив символов
}
else if (digits.Contains(expression[i].ToString()) || (expression[i] == ',' && lastSymbol.IndexOf(",") == -1)){
lastSymbol += expression[i];
} // если встретили цифру - добавляем ее в специальную переменную, чтобы не разделять число
else if(operands.Contains(expression[i].ToString())) {
if (lastSymbol != ""){
symbols.Add(lastSymbol);
lastSymbol = "";
}
if (symbols.Count > 0 && operands.Contains(symbols[symbols.Count - 1]) || symbols.Count == 0) {
string number = "";
switch (expression[i].ToString()) {
case "-": number += "-"; break;
case "+": number += "+"; break;
}
i++;
while (i < expression.Length && digits.Contains(expression[i].ToString())){
number += expression[i];
i++;
}
symbols.Add(number);
i--;
} // Если встретили "-" или "+", то проверяем - это знак арифметической операции или знак числа
else symbols.Add(expression[i].ToString());
}else{
lastFunction += expression[i].ToString().ToLower(); // Если ни одно условие не прошло => перед нами функция или константа
if (constants.Contains(lastFunction)) {
symbols.Add(constantsValues[lastFunction]);
lastFunction = "";
} // Если перед нами константа - добавляем ее в список символов как значение
else if (functions.Contains(lastFunction))
{
int functionStart = i + 1; // Находим первую скобку
int functionEnd = 0;
int bracketsSum = 1;
for (int j = functionStart + 1; j < expression.Length; j++)
{
if (expression[j].ToString() == "(") bracketsSum++;
if (expression[j].ToString() == ")") bracketsSum--;
if (bracketsSum == 0)
{
functionEnd = j;
i = functionEnd;
break;
}
} // Находим последнюю скобку. Так сложно сделано из-за того, что функции могут быть вложенными
char[] buffer = new char[functionEnd - functionStart - 1];
expression.CopyTo(functionStart + 1, buffer, 0, functionEnd - functionStart - 1);
string functionParametrs = new string(buffer);
if (lastFunction == "sqrt"){
var parametrs = GetParametrs(functionParametrs);
symbols.Add(Math.Pow(CalculateExpression(parametrs[0]), 1 / CalculateExpression(parametrs[1])).ToString());
}
if (lastFunction == "log"){
var parametrs = GetParametrs(functionParametrs);
symbols.Add(Math.Log(CalculateExpression(parametrs[0]), CalculateExpression(parametrs[1])).ToString());
}
if (lastFunction == "sin") symbols.Add(Math.Sin(CalculateExpression(functionParametrs)).ToString());
if (lastFunction == "cos") symbols.Add(Math.Cos(CalculateExpression(functionParametrs)).ToString());
if (lastFunction == "abs") symbols.Add(Math.Abs(CalculateExpression(functionParametrs)).ToString());
// Рассчитываем функцию рекурсивно
lastFunction = "";
}
}
}
if (lastSymbol != ""){
symbols.Add(lastSymbol);
lastSymbol = "";
} // Если последним символом была цифра, не забываем его добавить в список
В примере проскакивала функция GetParametrs. Она нужна для случаев, когда функция имеет 2 параметра. Дело в том, что нельзя сделать простой Split. У нас может быть такое выражение:
public static List<string> GetParametrs(string functionParametrs){
int bracketsSum = 0;
int functionEnd = 0;
for (int j = 0; j < functionParametrs.Length; j++){
if (functionParametrs[j].ToString() == "(") bracketsSum++;
if (functionParametrs[j].ToString() == ")") bracketsSum--;
if (functionParametrs[j].ToString() == ";" && bracketsSum == 0){
functionEnd = j;
break;
}
}
var buffer = new char[functionEnd];
functionParametrs.CopyTo(0, buffer, 0, functionEnd);
string firstParametr = new string(buffer);
buffer = new char[functionParametrs.Length - functionEnd - 1];
functionParametrs.CopyTo(functionEnd + 1, buffer, 0, functionParametrs.Length - functionEnd - 1);
string secondParametr = new string(buffer);
return ( new List<string>() { firstParametr, secondParametr } );
}
Итак, выражение разбито на более мелкие, и кроме того, значения функций уже вычислены и подставлены в основное выражение как числа.
Скобки можно обработать по тому же принципу — сразу рассчитывать их и подставлять их как числа:
while (symbols.Contains("(")) {
int bracketsStart = 0;
int bracketsEnd = 0;
int bracketsSum = 0;
for (int i = 0; i < symbols.Count; i++) {
if (symbols[i] == "(") {
bracketsStart = i;
bracketsSum = 1;
break;
}
}
for (int i = bracketsStart + 1; i < symbols.Count; i++) {
if (symbols[i] == "(") bracketsSum++;
if (symbols[i] == ")") bracketsSum--;
if (bracketsSum == 0) {
bracketsEnd = i;
break;
}
}
string bracketsExpression = "";
for (int i = bracketsStart + 1; i < bracketsEnd; i++) bracketsExpression += symbols[i];
symbols[bracketsStart] = CalculateExpression(bracketsExpression).ToString();
symbols.RemoveRange(bracketsStart + 1, bracketsEnd - bracketsStart);
}
С нахождением индекса закрывающейся скобки опять все немного сложнее. Нельзя просто взять следующую закрывающуюся скобку. Это не с��аботает для вложенных скобок.
Основная часть программы написана. Осталось реализовать расчет обычных арифметических выражений. Чтобы не париться с порядком выполнения действий, я решил записывать выражение в польской нотации:
foreach(var j in operands){ // Порядок выполнения операций зависит от порядка расположения знаков в массиве operands
var flagO = true;
while (flagO){
flagO = false;
for (int i = 0; i < symbols.Count; i++){
if (symbols[i] == j){
symbols[i - 1] = symbols[i - 1] + " " + symbols[i + 1] + " " + j;
symbols.RemoveRange(i, 2);
flagO = true;
break;
}
}
}
}
Ну и наконец, стеком рассчитываем значение выражения:
List<string> result = new List<string>();
string[] temp = symbols[0].Split(' ');
for (int i = 0; i < temp.Length; i++) {
if (operands.Contains(temp[i])) {
if (temp[i] == "^") {
result[result.Count - 2] = Math.Pow(double.Parse(result[result.Count - 2]), double.Parse(result[result.Count - 1])).ToString();
result.RemoveRange(result.Count - 1, 1);
}
if (temp[i] == "+") {
result[result.Count - 2] = (double.Parse(result[result.Count - 2]) + double.Parse(result[result.Count - 1])).ToString();
result.RemoveRange(result.Count - 1, 1);
}
if (temp[i] == "-") {
result[result.Count - 2] = (double.Parse(result[result.Count - 2]) - double.Parse(result[result.Count - 1])).ToString();
result.RemoveRange(result.Count - 1, 1);
}
if (temp[i] == "*") {
result[result.Count - 2] = (double.Parse(result[result.Count - 2]) * double.Parse(result[result.Count - 1])).ToString();
result.RemoveRange(result.Count - 1, 1);
}
if (temp[i] == "/") {
result[result.Count - 2] = (double.Parse(result[result.Count - 2]) / double.Parse(result[result.Count - 1])).ToString();
result.RemoveRange(result.Count - 1, 1);
}
}
else result.Add(temp[i]);
}
Если всё прошло хорошо, в result[0] будет лежать результат.
→ Ссылка на GitHub с полным кодом