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

Калькулятор на основе Обратной Польской записи

Здравствуйте друзья! Хочу вам рассказать об использовании алгоритма Обратной польской записи для создания калькулятора на языке C#.
О самом алгоритме рассказывать не буду, так как такая тема уже поднималась. Заострю внимание на конкретной программе.

Об алгоритме


Алгоритм Обратной польской нотации (ОПН) — форма записи математических выражений, в которой операнды расположены перед знаками операций. Также именуется как обратная польская запись, обратная бесскобочная запись (ОБЗ). © Wikipedia


Наша программа будет состоять из двух частей:
1) Класс RPN (Reverse Polish Notation) — тут будет реализована работа алгоритма
2) Класс Program — тут, собственно, будет реализовано использование класса RPN — писать будем в консоли, что бы было понятнее.



Часть первая: класс RPN


Класс будет содержать ряд методов:
class RPN
{
    //Метод Calculate принимает выражение в виде строки и возвращает результат, в своей работе использует другие методы класса
    static public double Calculate(string input)
    { ... }

    //Метод, преобразующий входную строку с выражением в постфиксную запись
    static private string GetExpression(string input)
    { ... }

    //Метод, вычисляющий значение выражения, уже преобразованного в постфиксную запись
    static private double Counting(string input)
    { ... }
}


Это 3 основных метода класса, опишем их подробнее:
Calculate — метод общедоступный, ему будет передаваться выражение в виде строки, и он будет возвращать результат вычисления. Результат он будет получать используя другие методы.
GetExpression — метод, которому основной метод Calculate будет передавать выражение, и получать его уже в постфиксной записи.
Counting — метод, который получая постфиксную запись выражения будет вычислять его результат.

Так же, помимо 3 основных методов, в классе будет еще 3 метода «обеспечения», это:
IsDelimeter — возвращает true, если проверяемый символ — разделитель
IsOperator — возвращает true, если проверяемый символ — оператор
GetPriority — метод возвращает приоритет проверяемого оператора, нужно для сортировки операторов

Сначала рассмотрим неосновные методы, они простые, так что проблем с пониманием быть не должно:

IsDelimeter:
//Метод возвращает true, если проверяемый символ - разделитель ("пробел" или "равно")
static private bool IsDelimeter(char c)
{
    if ((" =".IndexOf(c) != -1))
        return true;
    return false;
}

IsOperator:
//Метод возвращает true, если проверяемый символ - оператор
static private bool IsOperator(char с)
{
    if (("+-/*^()".IndexOf(с) != -1))
        return true;
    return false;
}

GetPriority:
//Метод возвращает приоритет оператора
static private byte GetPriority(char s)
{
    switch (s)
    {
        case '(': return 0;
        case ')': return 1;
        case '+': return 2;
        case '-': return 3;
        case '*': return 4;
        case '/': return 4;
        case '^': return 5;
        default: return 6;
    }
}


А теперь основные методы:
Calculate:
//"Входной" метод класса
static public double Calculate(string input)
{
    string output = GetExpression(input); //Преобразовываем выражение в постфиксную запись
    double result = Counting(output); //Решаем полученное выражение
    return result; //Возвращаем результат
}

GetExpression:
static private string GetExpression(string input)
{
    string output = string.Empty; //Строка для хранения выражения
    Stack<char> operStack = new Stack<char>(); //Стек для хранения операторов

    for (int i = 0; i < input.Length; i++) //Для каждого символа в входной строке
    {
        //Разделители пропускаем
        if (IsDelimeter(input[i]))
             continue; //Переходим к следующему символу
        
        //Если символ - цифра, то считываем все число
        if (Char.IsDigit(input[i])) //Если цифра
        {
             //Читаем до разделителя или оператора, что бы получить число
             while (!IsDelimeter(input[i]) && !IsOperator(input[i]))
             {
                 output += input[i]; //Добавляем каждую цифру числа к нашей строке
                 i++; //Переходим к следующему символу

                 if (i == input.Length) break; //Если символ - последний, то выходим из цикла
             }

             output += " "; //Дописываем после числа пробел в строку с выражением
             i--; //Возвращаемся на один символ назад, к символу перед разделителем
        }

        //Если символ - оператор
        if (IsOperator(input[i])) //Если оператор
        {
            if (input[i] == '(') //Если символ - открывающая скобка
                operStack.Push(input[i]); //Записываем её в стек
            else if (input[i] == ')') //Если символ - закрывающая скобка
            {
                //Выписываем все операторы до открывающей скобки в строку
                char s = operStack.Pop();

                while (s != '(')
                {
                    output += s.ToString() + ' ';
                    s = operStack.Pop();
                }
            }
            else //Если любой другой оператор
            {
                if (operStack.Count > 0) //Если в стеке есть элементы
                    if (GetPriority(input[i]) <= GetPriority(operStack.Peek())) //И если приоритет нашего оператора меньше или равен приоритету оператора на вершине стека
                        output += operStack.Pop().ToString() + " "; //То добавляем последний оператор из стека в строку с выражением

                operStack.Push(char.Parse(input[i].ToString())); //Если стек пуст, или же приоритет оператора выше - добавляем операторов на вершину стека

            }
        }
    }

    //Когда прошли по всем символам, выкидываем из стека все оставшиеся там операторы в строку
    while (operStack.Count > 0)
        output += operStack.Pop() + " ";

    return output; //Возвращаем выражение в постфиксной записи
}

Counting:
static private double Counting(string input)
{
    double result = 0; //Результат
    Stack<double> temp = new Stack<double>(); //Dhtvtyysq стек для решения

    for (int i = 0; i < input.Length; i++) //Для каждого символа в строке
    {
        //Если символ - цифра, то читаем все число и записываем на вершину стека
        if (Char.IsDigit(input[i])) 
        {
            string a = string.Empty;

            while (!IsDelimeter(input[i]) && !IsOperator(input[i])) //Пока не разделитель
            {
                a += input[i]; //Добавляем
                i++;
                if (i == input.Length) break;
            }
            temp.Push(double.Parse(a)); //Записываем в стек
            i--;
        }
        else if (IsOperator(input[i])) //Если символ - оператор
        {
            //Берем два последних значения из стека
            double a = temp.Pop(); 
            double b = temp.Pop();

            switch (input[i]) //И производим над ними действие, согласно оператору
            { 
                case '+': result = b + a; break;
                case '-': result = b - a; break;
                case '*': result = b * a; break;
                case '/': result = b / a; break;
                case '^': result = double.Parse(Math.Pow(double.Parse(b.ToString()), double.Parse(a.ToString())).ToString()); break;
            }
            temp.Push(result); //Результат вычисления записываем обратно в стек
        }
    }
    return temp.Peek(); //Забираем результат всех вычислений из стека и возвращаем его
}

Наш класс готов!

Часть 2: класс Program


Тут все просто, думаю объяснять большого смысла нет, просто прокомментирую:
class Program
{
    static void Main(string[] args)
    {
        while (true) //Бесконечный цикл
        {
            Console.Write("Введите выражение: "); //Предлагаем ввести выражение
            Console.WriteLine(RPN.Calculate(Console.ReadLine())); //Считываем, и выводим результат
        }
    }
}


Внимание! Ошибки!
Хочу обратить ваше внимание на одну вещь! В нашем классе я не реализовывал проверку входных данных, поэтому при некорректном вводе будут вылетать исключения!
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.