Постановка задачи: создать калькулятор с графическим интерфейсом и возможностью вычисления длинных выражений (для чего не годится простой калькулятор с ограниченным числом операндов) с помощью ЯП Java и RIA JavaFX. Хочу заметить, что задача алгоритмическая и может быть реализована на любом языке программирования, я выбрал Java лишь из-за удобства его графического интерфейса.

Поделим алгоритм на 3 части: разбиение исходной строки на массив лексем, перевод этого массива в обратную польскую запись, вычисление.

  1. Разбиение исходной строки на массив лексем. Условимся ограничивать поле ввода ( fx:id = "label") 30 символами. При достижении 30-го символа поле ввода блокируется и становятся доступными лишь кнопки "С" и "СЕ". Обработка нажатий на кнопку выглядит следующим образом:

@FXML
    public void click6() {
        if (text.length() >= 30)
            return;
        text += "6";
        label.setText(text);
    }

Некоторые поля класса Controller:

    @FXML
    private Label label;  // поле ввода         
    @FXML
    private Label answer;   // поле ответа
    private String text = "";   // обрабатываемая введенная строка 
    private String[] lec; // массив лексем

"Проходя" по введенной строке, будем считать количество скобок (обработаем ошибку, когда число левых скобок не равно числу правых скобок). Стоит обратить внимание на следующие моменты:

  • Операции "sqrt" и "-" (в отдельных случаях) - унарные. Так уж получилось, что чем раньше проанализировать это свойство, тем легче его в итоге получится реализовать. В случае минуса будем просто добавлять перед ним 0. Это будет работать, ведь мы удостоверимся, что данный минус унарный. В случае квадратного корня не будем считать его отдельной операцией - конвертируем по схеме "sqrt(x) -> (x)^0.5", где x - какое-либо число. Теперь крайне важно найти нужную закрывающую скобку, которая относится именно к операции корня. Чтобы это сделать, реализуем класс Level, в котором будем поле "mode", и объявим массив объектов этого класса. Тогда каждой скобке будет соответствовать свое состояние false или true (нужен флаг корня, который будет принимать true в случае нахождения в строке символа 's'). Также добавим level_counter, который будет увеличиваться на 1 при '(' и уменьшаться при ')'. Класс Level определим следующим образом:

class Level{
    private boolean mode;
    Level(boolean mode){
        this.mode = mode;
    }
    public void set_mode(boolean mode){
        this.mode = mode;
    }
    public boolean get_mode(){
        return mode;
    }
}
  • В цикле for необходимы 2 счетчика: классический i (проход по введенной строке) и point (заполнения массива lec[]). Условие завершения: символ не равен 'f', который предварительно был объявлен как последний.

  • Обработаем ошибку нескольких точек в одном числе. Для этого объявим dot_counter, увеличивающийся на 1 при нахождении точки в одном и том же числе.

  • Чтобы в операциях "sqrt()" и "mod()" стирать не по отдельной букве, а целиком всю операцию, модифицируем метод clickC() следующим образом:

@FXML
    public void clickC() {
        StringBuilder s = new StringBuilder(text);
        if (s.length() > 1 && (s.charAt(s.length() - 2) == 't' || s.charAt(s.length() - 2) == 'd')){
            if (s.charAt(s.length() - 2) == 't'){
                for(int i = 0; i <= 4; i++)
                    s.deleteCharAt(s.length() - 1);
            }
            else if(s.charAt(s.length() - 2) == 'd'){
                for(int i = 0; i <= 3; i++)
                    s.deleteCharAt(s.length() - 1);
            }
        }
        else if (s.length() > 0)
                s.deleteCharAt(s.length() - 1);
        text = s.toString();
        label.setText(text);
    }

Итак, на выходе из цикла получаем заполненный массив lec[], где каждый элемент - дискретный и удобный для дальнейших этапов.

  1. Перевод массива в обратную польскую запись. ОПЗ уже давно доказала свою простоту и эффективность, так что я без раздумий решился реализовать именно этот вариант обработки строки. Однако можно было и построить бинарное дерево, как, например, тут.
    Цель на данном этапе – получить выходную строку (output в коде) в постфиксной форме. Подробно можно почитать здесь. Все изложено грамотно и доступно. Главное преимущество ОПЗ заключается в отсутствии скобок. Процессор производит вычисление, когда “видит” 2 числа и знак операции после них. Так как унарные операции уже обработаны, предполагаем, что на вход поступают только корректные данные. Чтобы избежать вечно раздражающего NullPointerException, заполним только что созданный стек символами ‘f’, т.к. этот символ не используется ни в одной из операций. Я отказался от традиционного массива приоритетов операций в силу операторов ветвления (получилось аналогично). Также нам понадобится переменная add (в операциях умножения и большего либо равного приоритета), отвечающая за переход в конец стека. При чистке стека заполняем очищаемые элементы символами ‘f’. Для операций сложения и вычитания понадобится флаг in_brackets, с помощью которого ‘+’ будет переходить на нужный элемент стека (за скобку, а не вместо нее).Таким образом, мы перешли от массива строк lec[] вида [‘(‘, ‘A’, ’+’, ’B’, ‘)’, ’*’, ’C’, …] к строке output вида “AB+C* …”, где A, B, C – некоторые числа. Остается лишь вычислить и записать ответ в поле answer.

  2. Вычисление. Создаем список Double. Так как в ОПЗ гарантируется, что сначала будет как минимум 2 числа, можно не боятся RunTime ошибок типа OutOfBonds и смело вычитать 1 или 2 в квадратных скобках. Вычисление происходит между двумя последними числами списка. Какое? Зависит от следующей строки output[].
    Из необычного, в моей реализации mod отвечает за остаток, тогда как ‘%’ – за целочисленное деление. Используем метод pow() класса Math для вычислений степеней. Особенности этого метода заключаются в том, что он может "выкидывать" исключительные значения, такие как "NaN" или "Infinity". Второй я решил оставить, а вот первый надо бы доработать. Как мы знаем, под корнем не может быть отрицательного числа. Но только под корнем четной степени. Эти степени 0.5, 0.25, 0.125 и т.д. Так, при возведении в степень отрицательного числа, pow(), если меньше единицы и больше нуля, то проверим четность степени корня путем цикличного домножения степени на 2. Если она станет равной 1, значит степень четная. Результат запишем в флаг odd_power. Если true, то возьмем модуль числа, возведем в степень и добавим унарный минус. Так я ушел от встроенных "ошибок" метода pow(). Также добавим обработку ошибки деления на 0. Тут все просто: если второй операнд 0, выводим в поле ответа специальную строку.

  3. Немного о дизайне. Интерфейс создавал в конструкторе SceneBuilder и, по моему скромному мнению, вышло довольно-таки органично. Для красоты добавим иконку: stage.getIcons().add(new Image("file:icon.png"));
    Результат:

    Дизайн итогового приложения
    Дизайн итогового приложения

Код метода обработчика нажатия на кнопку "=":

@FXML
    public void click_equals() {
        if (text.length() >= 30)
            return;
        int[] dot_counter = new int[30];
        int left_bracket_counter = 0, right_bracket_counter = 0;
        StringBuilder s = new StringBuilder(text);
        s.insert(s.length(), "f");
        lec = new String[30];          // наполнение массива лексем
        int level_counter = 0;
        Level[] level = new Level[30];
        boolean sqrt_flag = false;
        for (int i = 0, point = 0; !String.valueOf(s.charAt(i)).equals("f"); i++, point++){
           lec[point] = "";
            if(String.valueOf(s.charAt(i)).equals("(")) {
                level[level_counter] = new Level(false);
                level[level_counter++].set_mode(sqrt_flag);
                sqrt_flag = false;
                left_bracket_counter++;
                lec[point] = "(";
            }
            else if(String.valueOf(s.charAt(i)).equals(")")) {
                if(level_counter > 0 && level[--level_counter].get_mode()){
                    lec[point++] = ")";
                    lec[point++] = "^";
                    lec[point] = "0.5";
                    right_bracket_counter++;
                }
                else {
                    right_bracket_counter++;
                    lec[point] = ")";
                }
            }
            else if (String.valueOf(s.charAt(i)).equals("s")) {
                sqrt_flag = true;
                point--;
                i += 3;
            }
            else if (String.valueOf(s.charAt(i)).equals("m")) {
                lec[point] = "";
                for (int j = i; j < i + 3; j++)
                    lec[point] += String.valueOf(s.charAt(j));
                i += 2;
            }
            else if(Character.isDigit(s.charAt(i)) || String.valueOf(s.charAt(i)).equals(".")){   // если цифра или точка
                    lec[point] += String.valueOf(s.charAt(i));
                while (Character.isDigit(s.charAt(++i)) || String.valueOf(s.charAt(i)).equals(".")){
                    lec[point]  += s.charAt(i);
                    if(String.valueOf(s.charAt(i)).equals("."))
                        dot_counter[point]++;
                }
                if (dot_counter[point] > 1){
                    for(int j = 0; j < 30; j++)
                        lec[j] = null;
                    answer.setText("Error_points");
                    dot_counter[point] = 0;
                    return;
                }
                i--;
            }
            else if(String.valueOf(s.charAt(i)).equals("+")){
                lec[point] = "+";
            }
            else if(String.valueOf(s.charAt(i)).equals("-")){
                if (i == 0 || !Character.isDigit(s.charAt(i - 1)) && !String.valueOf(s.charAt(i - 1)).equals(")")){
                    lec[point++] = "0";
                    lec[point] = "-";
                }
                else lec[point] = "-";
            }
            else if(String.valueOf(s.charAt(i)).equals("*")){
                lec[point] = "*";
            }
            else if(String.valueOf(s.charAt(i)).equals("/")){
                lec[point] = "/";
            }
            else if(String.valueOf(s.charAt(i)).equals("%")){
                lec[point] = "%";
            }
            else if(String.valueOf(s.charAt(i)).equals("^")){
                lec[point] = "^";
            }
        }
        if (right_bracket_counter != left_bracket_counter){
            for(int i = 0; i < 30; i++)
                lec[i] = null;
            answer.setText("Error_brackets");
            return;
        }
        System.out.println(Arrays.toString(lec));

        //----------------------------перевод в обр. Польскую запись------------------------------------

        String[] output = new String[30];
        int output_pointer = 0;
        String[] stack = new String[30];
        int stack_pointer = 0;
        int add = 0;
        for (int i = 0; i < 30; i++)
            stack[i] = "f";
        boolean in_brackets = false;
        for (int i = 0; i < lec.length && lec[i] != null; i++){
            if(lec[i].equals("(")){
                stack[stack_pointer++] = lec[i];
            }
            else if(lec[i].equals(")")){
                while (!stack[--stack_pointer].equals("(")){
                    output[output_pointer++] = stack[stack_pointer];
                    stack[stack_pointer] = "f";
                }
                stack[stack_pointer] = "f";
            }
            else if(lec[i].equals("+")){
                while (!stack[stack_pointer].equals("(") && stack_pointer != 0){
                    stack[stack_pointer--] = "f";
                    if (stack[stack_pointer].equals("(")) {
                        in_brackets = true;
                        break;
                    }
                    output[output_pointer++] = stack[stack_pointer];
                }
                if (in_brackets) {
                    stack[++stack_pointer] = "+";
                    stack_pointer++;
                }
                else stack[stack_pointer++] = "+";
                in_brackets = false;
            }
            else if(lec[i].equals("-")){
                    while (!stack[stack_pointer].equals("(") && stack_pointer != 0) {
                        stack[stack_pointer--] = "f";
                        if (stack[stack_pointer].equals("(")) {
                            in_brackets = true;
                            break;
                        }
                        output[output_pointer++] = stack[stack_pointer];
                    }
                    if (in_brackets) {
                        stack[++stack_pointer] = "-";
                        stack_pointer++;
                    } else stack[stack_pointer++] = "-";
                    in_brackets = false;
                }
            else if(lec[i].equals("*")){
                add = 0;
                while (stack_pointer >= 0 && !stack[stack_pointer].equals("(") ){
                    if(stack[stack_pointer].equals("+") || stack[stack_pointer].equals("-")){
                        add++;
                        stack_pointer--;
                        continue;
                    }
                    else if(stack[stack_pointer].equals("*") || stack[stack_pointer].equals("/") || stack[stack_pointer].equals("%") || stack[stack_pointer].equals("mod") || stack[stack_pointer].equals("^")) {
                        output[output_pointer++] = stack[stack_pointer];
                    }
                    stack[stack_pointer--] = "f";
                }
                if (stack_pointer < 0)
                    stack_pointer = 0;
                stack_pointer += add;
                if (!stack[stack_pointer].equals("f"))
                    stack_pointer++;
                stack[stack_pointer++] = "*";
            }
            else if(lec[i].equals("/")){
                add = 0;
                while (stack_pointer >= 0 && !stack[stack_pointer].equals("(") ){
                    if(stack[stack_pointer].equals("+") || stack[stack_pointer].equals("-")){
                        add++;
                        stack_pointer--;
                        continue;
                    }
                    else if(stack[stack_pointer].equals("*") || stack[stack_pointer].equals("/") || stack[stack_pointer].equals("%") || stack[stack_pointer].equals("mod") || stack[stack_pointer].equals("^")) {  //todo для возведения в степень тоже
                        output[output_pointer++] = stack[stack_pointer];
                    }
                    stack[stack_pointer--] = "f";
                }
                if (stack_pointer < 0)
                    stack_pointer = 0;
                stack_pointer += add;
                if (!stack[stack_pointer].equals("f"))
                    stack_pointer++;
                stack[stack_pointer++] = "/";
            }
            else if(lec[i].equals("^")){
                add = 0;
                while (!stack[stack_pointer].equals("(") && stack_pointer != 0){
                    if(stack[stack_pointer].equals("^")) {
                        output[output_pointer++] = stack[stack_pointer];
                        stack[stack_pointer--] = "f";
                    }
                    else {
                        add++;
                        stack_pointer--;
                    }
                }
                stack_pointer += add;
                if(!stack[stack_pointer].equals("f"))
                    stack_pointer++;
                stack[stack_pointer++] = "^";
            }
            else if(lec[i].equals("%")){
                add = 0;
                while (stack_pointer >= 0 && !stack[stack_pointer].equals("(") ){
                    if(stack[stack_pointer].equals("+") || stack[stack_pointer].equals("-")){
                        add++;
                        stack_pointer--;
                        continue;
                    }
                    else if(stack[stack_pointer].equals("*") || stack[stack_pointer].equals("/") || stack[stack_pointer].equals("%") || stack[stack_pointer].equals("mod") || stack[stack_pointer].equals("^")) {
                        output[output_pointer++] = stack[stack_pointer];
                    }
                    stack[stack_pointer--] = "f";
                }
                if (stack_pointer < 0)
                    stack_pointer = 0;
                stack_pointer += add;
                if (!stack[stack_pointer].equals("f"))
                    stack_pointer++;
                stack[stack_pointer++] = "%";
            }
            else if(lec[i].equals("mod")){
                add = 0;
                while (stack_pointer >= 0 && !stack[stack_pointer].equals("(") ){
                    if(stack[stack_pointer].equals("+") || stack[stack_pointer].equals("-")){
                        add++;
                        stack_pointer--;
                        continue;
                    }
                    else if(stack[stack_pointer].equals("*") || stack[stack_pointer].equals("/") || stack[stack_pointer].equals("%") || stack[stack_pointer].equals("mod") || stack[stack_pointer].equals("^")) {  //todo для возведения в степень тоже
                        output[output_pointer++] = stack[stack_pointer];
                    }
                    stack[stack_pointer--] = "f";
                }
                if (stack_pointer < 0)
                    stack_pointer = 0;
                stack_pointer += add;
                if (!stack[stack_pointer].equals("f"))
                    stack_pointer++;
                stack[stack_pointer++] = "mod";
            }
            else if(Character.isDigit(lec[i].charAt(0)) || String.valueOf(lec[i].charAt(0)).equals(".")){
                output[output_pointer++] = lec[i];
            }
            System.out.println(Arrays.toString(stack)); // в отладночных целях
        }
        while (stack_pointer > 0)
            output[output_pointer++] = stack[--stack_pointer];

        System.out.println(Arrays.toString(output));  // в отладночных целях
        //-------------------------------вычисление----------------------------------
        output_pointer = 0;

        ArrayList<Double> var1 = new ArrayList<>();
        var1.add(Double.parseDouble(output[output_pointer++]));
            while(output[output_pointer] != null){
            if (Character.isDigit(output[output_pointer].charAt(0)) || String.valueOf(output[output_pointer].charAt(0)).equals(".")){
                var1.add(Double.parseDouble(output[output_pointer++]));
            }
            else if (output[output_pointer].equals("+")){

                var1.set(var1.size() - 2, var1.get(var1.size() - 2) + var1.get(var1.size() - 1));
                var1.remove(var1.size() - 1);
                output_pointer++;
            }
            else if (output[output_pointer].equals("-")){

                var1.set(var1.size() - 2, var1.get(var1.size() - 2) - var1.get(var1.size() - 1));
                var1.remove(var1.size() - 1);
                output_pointer++;
            }
            else if (output[output_pointer].equals("*")){
                var1.set(var1.size() - 2, var1.get(var1.size() - 2) * var1.get(var1.size() - 1));
                var1.remove(var1.size() - 1);
                output_pointer++;
            }
            else if (output[output_pointer].equals("/")){
                //if (var[var_pointer - 1].get_level() == var[var_pointer].get_level()) {
                if (var1.get(var1.size() - 1) == 0.0){
                    answer.setText("Division_zero_error");
                    return;
                }
                var1.set(var1.size() - 2, var1.get(var1.size() - 2) / var1.get(var1.size() - 1));
                var1.remove(var1.size() - 1);
                output_pointer++;
            }
            else if (output[output_pointer].equals("mod")){  // остаток
                if (var1.get(var1.size() - 1) == 0.0){
                    answer.setText("Division_zero_error");
                    return;
                }
                var1.set(var1.size() - 2, var1.get(var1.size() - 2) % var1.get(var1.size() - 1));
                var1.remove(var1.size() - 1);
                output_pointer++;
            }
            else if (output[output_pointer].equals("%")){   // целоцисленное деление
                if (var1.get(var1.size() - 1) == 0.0){
                    answer.setText("Division_zero_error");
                    return;
                }
                var1.set(var1.size() - 2, 1.0 * (Math.round(var1.get(var1.size() - 2)) / Math.round(var1.get(var1.size() - 1))));
                var1.remove(var1.size() - 1);
                output_pointer++;
            }
            else if (output[output_pointer].equals("^")){
                boolean odd_power = false;
                if (var1.get(var1.size() - 2) < 0.0 && var1.get(var1.size() - 1) < 1.0 && var1.get(var1.size() - 1) > 0.0){
                    for (double a = var1.get(var1.size()- 1); a <= 1.0; a *= 2)
                        if (a == 1.0){
                            answer.setText("Negative_num_in_even_power");
                            return;
                        }
                    odd_power = true;
                }
                if (odd_power)
                var1.set(var1.size() - 2, -Math.pow(Math.abs(var1.get(var1.size() - 2)), var1.get(var1.size() - 1)));
                else var1.set(var1.size() - 2, Math.pow(var1.get(var1.size() - 2), var1.get(var1.size() - 1)));
                var1.remove(var1.size() - 1);
                output_pointer++;
            }
        }
        answer.setText(var1.get(0).toString());
    }

Вывод: я успешно реализовал алгоритм оценки математического выражения, создав рабочий инженерный калькулятор с графическим интерфейсом RIA JavaFX, поработал с некоторыми возможными ошибками и, что самое главное, узнал много нового и полезного.

P.S. Я знаю, что итоговый алгоритм получился не слишком оптимальным, при желании можно найти в нем не один баг, поэтому любые правки и предложения (а также вопросы) приветствуются в комментариях.

Комментарии (6)


  1. lorc
    24.04.2023 13:42
    +7

    Если это - лабораторная работа - то в принципе вопросов особых нет. Но лучше в самом начале текста сделать дисклеймер типа "это учебная работа, не повторяйте это в реальной жизни". Потому как если относится к этому как к продакшен коду - то вопросов и замечаний тут просто вагон. Но вы и сами поймете все по мере того как будете получать все больше практики и опыта.


    1. 40in_studios_owner Автор
      24.04.2023 13:42
      +1

      Вы правы, это действительно лабораторная работа, расширенная в такой мини-проект)

      Спасибо за отзыв, буду работать.


      1. dyadyaSerezha
        24.04.2023 13:42
        +1

        Чисто для начала.

        1) if идет со скобкой то с пробелом, то без пробела. За такой "стиль" сразу будут бить.

        2) Очень часто берется последний и предпоследний элементы ArrayList. Можно хотя бы сделать новый класс от него и добавить методы getLast и getBeforeLast.

        3) Название неверное, так как JavaFX вообще не используется для вычисления выражения.


        1. lorc
          24.04.2023 13:42

          Ну это у вас мелкие придирки :)

          Там чисто с архитектурной точки зрения много всякого: было бы круто сделать лексер, чтобы работать с потоком лексем, а не символов. Это позволило бы не парится подсчетом количества точек, например. Точнее - может и парится, но на другом уровне абстракции. Вообще очень полезно разделять код по разным уровням абстракции. Дикие лестницы из if/else наверное можно было бы заменить таблицей из методов (не знаю, позволяет ли Java вызывать методы по ссылке) или, если лексер будет построен по всем правилам ООП, - то обойтись наследованием и виртуальными методами (хотя, это уже начинает пахнуть фабриками фабрик).

          Ну и всякие банальные советы по выносу одинакового кода в общие функции. Типа, если хендлер каждой кнопки выглядит вот так:

              public void click6() {
                  if (text.length() >= 30)
                      return;
                  text += "6";
                  label.setText(text);
              }

          То это "фу". Надо сделать вспомогательный метод и дергать его:

              public void append_display(string ch) {
                  if (text.length() >= 30)
                      return;
                  text += ch;
                  label.setText(text);
                
                }
              public void click6() {
                append_display("6");
              }

          Кода станет меньше, а с ним - и будет меньше потенциальных ошибок. Можно пойти еще дальше и вообще не писать отдельный хендлер на каждую кнопку. Большинство UI тулкитов позволяют назначать один хендлер на кучу кнопок и там внутри уже определять что было нажато. Надеюсь, JavaFX тоже так может.

          Ну и за стилем кода тоже надо следить, конечно же...


          1. dyadyaSerezha
            24.04.2023 13:42

            Я посмотрел код очень быстро и не стал писать про длинные методы или внутреннюю логику, лексер и так далее. Написал лишь то, что лежало на поверхности.)


        1. KvanTTT
          24.04.2023 13:42

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