Pull to refresh

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

Здравствуйте друзья! Хочу вам рассказать об использовании алгоритма Обратной польской записи для создания калькулятора на языке 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())); //Считываем, и выводим результат
        }
    }
}


Внимание! Ошибки!
Хочу обратить ваше внимание на одну вещь! В нашем классе я не реализовывал проверку входных данных, поэтому при некорректном вводе будут вылетать исключения!
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.